mirror of https://github.com/dsoprea/go-exif.git
Seed v3 release. Copy from v2 release.
parent
e0ce96b49e
commit
56058635d0
|
@ -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.
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
After Width: | Height: | Size: 193 KiB |
File diff suppressed because one or more lines are too long
|
@ -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
|
Binary file not shown.
|
@ -0,0 +1,663 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
ifdLogger = log.NewLogger("exifcommon.ifd")
|
||||
)
|
||||
|
||||
var (
|
||||
ErrChildIfdNotMapped = errors.New("no child-IFD for that tag-ID under parent")
|
||||
)
|
||||
|
||||
// MappedIfd is one node in the IFD-mapping.
|
||||
type MappedIfd struct {
|
||||
ParentTagId uint16
|
||||
Placement []uint16
|
||||
Path []string
|
||||
|
||||
Name string
|
||||
TagId uint16
|
||||
Children map[uint16]*MappedIfd
|
||||
}
|
||||
|
||||
// String returns a descriptive string.
|
||||
func (mi *MappedIfd) String() string {
|
||||
pathPhrase := mi.PathPhrase()
|
||||
return fmt.Sprintf("MappedIfd<(0x%04X) [%s] PATH=[%s]>", mi.TagId, mi.Name, pathPhrase)
|
||||
}
|
||||
|
||||
// PathPhrase returns a non-fully-qualified IFD path.
|
||||
func (mi *MappedIfd) PathPhrase() string {
|
||||
return strings.Join(mi.Path, "/")
|
||||
}
|
||||
|
||||
// TODO(dustin): Refactor this to use IfdIdentity structs.
|
||||
|
||||
// IfdMapping describes all of the IFDs that we currently recognize.
|
||||
type IfdMapping struct {
|
||||
rootNode *MappedIfd
|
||||
}
|
||||
|
||||
// NewIfdMapping returns a new IfdMapping struct.
|
||||
func NewIfdMapping() (ifdMapping *IfdMapping) {
|
||||
rootNode := &MappedIfd{
|
||||
Path: make([]string, 0),
|
||||
Children: make(map[uint16]*MappedIfd),
|
||||
}
|
||||
|
||||
return &IfdMapping{
|
||||
rootNode: rootNode,
|
||||
}
|
||||
}
|
||||
|
||||
// NewIfdMappingWithStandard retruns a new IfdMapping struct preloaded with the
|
||||
// standard IFDs.
|
||||
func NewIfdMappingWithStandard() (ifdMapping *IfdMapping) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err := log.Wrap(state.(error))
|
||||
log.Panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// RELEASE(dustin): Add error return on next release
|
||||
|
||||
im := NewIfdMapping()
|
||||
|
||||
err := LoadStandardIfds(im)
|
||||
log.PanicIf(err)
|
||||
|
||||
return im
|
||||
}
|
||||
|
||||
// Get returns the node given the path slice.
|
||||
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
|
||||
}
|
||||
|
||||
// GetWithPath returns the node given the path string.
|
||||
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
|
||||
}
|
||||
|
||||
// IfdTagIdAndIndex represents a specific part of the IFD path.
|
||||
//
|
||||
// This is a legacy type.
|
||||
type IfdTagIdAndIndex struct {
|
||||
Name string
|
||||
TagId uint16
|
||||
Index int
|
||||
}
|
||||
|
||||
// String returns a descriptive string.
|
||||
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
|
||||
}
|
||||
|
||||
// FqPathPhraseFromLineage returns the fully-qualified IFD path from the slice.
|
||||
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, "/")
|
||||
}
|
||||
|
||||
// PathPhraseFromLineage returns the non-fully-qualified IFD path from the
|
||||
// slice.
|
||||
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
|
||||
}
|
||||
|
||||
// DumpLineages returns a slice of strings representing all mappings.
|
||||
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
|
||||
}
|
||||
|
||||
// LoadStandardIfds loads the standard IFDs into the mapping.
|
||||
func LoadStandardIfds(im *IfdMapping) (err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
err = im.Add(
|
||||
[]uint16{},
|
||||
IfdStandardIfdIdentity.TagId(), IfdStandardIfdIdentity.Name())
|
||||
|
||||
log.PanicIf(err)
|
||||
|
||||
err = im.Add(
|
||||
[]uint16{IfdStandardIfdIdentity.TagId()},
|
||||
IfdExifStandardIfdIdentity.TagId(), IfdExifStandardIfdIdentity.Name())
|
||||
|
||||
log.PanicIf(err)
|
||||
|
||||
err = im.Add(
|
||||
[]uint16{IfdStandardIfdIdentity.TagId(), IfdExifStandardIfdIdentity.TagId()},
|
||||
IfdExifIopStandardIfdIdentity.TagId(), IfdExifIopStandardIfdIdentity.Name())
|
||||
|
||||
log.PanicIf(err)
|
||||
|
||||
err = im.Add(
|
||||
[]uint16{IfdStandardIfdIdentity.TagId()},
|
||||
IfdGpsInfoStandardIfdIdentity.TagId(), IfdGpsInfoStandardIfdIdentity.Name())
|
||||
|
||||
log.PanicIf(err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IfdTag describes a single IFD tag and its parent (if any).
|
||||
type IfdTag struct {
|
||||
parentIfdTag *IfdTag
|
||||
tagId uint16
|
||||
name string
|
||||
}
|
||||
|
||||
func NewIfdTag(parentIfdTag *IfdTag, tagId uint16, name string) IfdTag {
|
||||
return IfdTag{
|
||||
parentIfdTag: parentIfdTag,
|
||||
tagId: tagId,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// ParentIfd returns the IfdTag of this IFD's parent.
|
||||
func (it IfdTag) ParentIfd() *IfdTag {
|
||||
return it.parentIfdTag
|
||||
}
|
||||
|
||||
// TagId returns the tag-ID of this IFD.
|
||||
func (it IfdTag) TagId() uint16 {
|
||||
return it.tagId
|
||||
}
|
||||
|
||||
// Name returns the simple name of this IFD.
|
||||
func (it IfdTag) Name() string {
|
||||
return it.name
|
||||
}
|
||||
|
||||
// String returns a descriptive string.
|
||||
func (it IfdTag) String() string {
|
||||
parentIfdPhrase := ""
|
||||
if it.parentIfdTag != nil {
|
||||
parentIfdPhrase = fmt.Sprintf(" PARENT=(0x%04x)[%s]", it.parentIfdTag.tagId, it.parentIfdTag.name)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("IfdTag<TAG-ID=(0x%04x) NAME=[%s]%s>", it.tagId, it.name, parentIfdPhrase)
|
||||
}
|
||||
|
||||
var (
|
||||
// rootStandardIfd is the standard root IFD.
|
||||
rootStandardIfd = NewIfdTag(nil, 0x0000, "IFD") // IFD
|
||||
|
||||
// exifStandardIfd is the standard "Exif" IFD.
|
||||
exifStandardIfd = NewIfdTag(&rootStandardIfd, 0x8769, "Exif") // IFD/Exif
|
||||
|
||||
// iopStandardIfd is the standard "Iop" IFD.
|
||||
iopStandardIfd = NewIfdTag(&exifStandardIfd, 0xA005, "Iop") // IFD/Exif/Iop
|
||||
|
||||
// gpsInfoStandardIfd is the standard "GPS" IFD.
|
||||
gpsInfoStandardIfd = NewIfdTag(&rootStandardIfd, 0x8825, "GPSInfo") // IFD/GPSInfo
|
||||
)
|
||||
|
||||
// IfdIdentityPart represents one component in an IFD path.
|
||||
type IfdIdentityPart struct {
|
||||
Name string
|
||||
Index int
|
||||
}
|
||||
|
||||
// String returns a fully-qualified IFD path.
|
||||
func (iip IfdIdentityPart) String() string {
|
||||
if iip.Index > 0 {
|
||||
return fmt.Sprintf("%s%d", iip.Name, iip.Index)
|
||||
} else {
|
||||
return iip.Name
|
||||
}
|
||||
}
|
||||
|
||||
// UnindexedString returned a non-fully-qualified IFD path.
|
||||
func (iip IfdIdentityPart) UnindexedString() string {
|
||||
return iip.Name
|
||||
}
|
||||
|
||||
// IfdIdentity represents a single IFD path and provides access to various
|
||||
// information and representations.
|
||||
//
|
||||
// Only global instances can be used for equality checks.
|
||||
type IfdIdentity struct {
|
||||
ifdTag IfdTag
|
||||
parts []IfdIdentityPart
|
||||
ifdPath string
|
||||
fqIfdPath string
|
||||
}
|
||||
|
||||
// NewIfdIdentity returns a new IfdIdentity struct.
|
||||
func NewIfdIdentity(ifdTag IfdTag, parts ...IfdIdentityPart) (ii *IfdIdentity) {
|
||||
ii = &IfdIdentity{
|
||||
ifdTag: ifdTag,
|
||||
parts: parts,
|
||||
}
|
||||
|
||||
ii.ifdPath = ii.getIfdPath()
|
||||
ii.fqIfdPath = ii.getFqIfdPath()
|
||||
|
||||
return ii
|
||||
}
|
||||
|
||||
// NewIfdIdentityFromString parses a string like "IFD/Exif" or "IFD1" or
|
||||
// something more exotic with custom IFDs ("SomeIFD4/SomeChildIFD6"). Note that
|
||||
// this will valid the unindexed IFD structure (because the standard tags from
|
||||
// the specification are unindexed), but not, obviously, any indices (e.g.
|
||||
// the numbers in "IFD0", "IFD1", "SomeIFD4/SomeChildIFD6"). It is
|
||||
// required for the caller to check whether these specific instances
|
||||
// were actually parsed out of the stream.
|
||||
func NewIfdIdentityFromString(im *IfdMapping, fqIfdPath string) (ii *IfdIdentity, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
lineage, err := im.ResolvePath(fqIfdPath)
|
||||
log.PanicIf(err)
|
||||
|
||||
var lastIt *IfdTag
|
||||
identityParts := make([]IfdIdentityPart, len(lineage))
|
||||
for i, itii := range lineage {
|
||||
// Build out the tag that will eventually point to the IFD represented
|
||||
// by the right-most part in the IFD path.
|
||||
|
||||
it := &IfdTag{
|
||||
parentIfdTag: lastIt,
|
||||
tagId: itii.TagId,
|
||||
name: itii.Name,
|
||||
}
|
||||
|
||||
lastIt = it
|
||||
|
||||
// Create the next IfdIdentity part.
|
||||
|
||||
iip := IfdIdentityPart{
|
||||
Name: itii.Name,
|
||||
Index: itii.Index,
|
||||
}
|
||||
|
||||
identityParts[i] = iip
|
||||
}
|
||||
|
||||
ii = NewIfdIdentity(*lastIt, identityParts...)
|
||||
return ii, nil
|
||||
}
|
||||
|
||||
func (ii *IfdIdentity) getFqIfdPath() string {
|
||||
partPhrases := make([]string, len(ii.parts))
|
||||
for i, iip := range ii.parts {
|
||||
partPhrases[i] = iip.String()
|
||||
}
|
||||
|
||||
return strings.Join(partPhrases, "/")
|
||||
}
|
||||
|
||||
func (ii *IfdIdentity) getIfdPath() string {
|
||||
partPhrases := make([]string, len(ii.parts))
|
||||
for i, iip := range ii.parts {
|
||||
partPhrases[i] = iip.UnindexedString()
|
||||
}
|
||||
|
||||
return strings.Join(partPhrases, "/")
|
||||
}
|
||||
|
||||
// String returns a fully-qualified IFD path.
|
||||
func (ii *IfdIdentity) String() string {
|
||||
return ii.fqIfdPath
|
||||
}
|
||||
|
||||
// UnindexedString returns a non-fully-qualified IFD path.
|
||||
func (ii *IfdIdentity) UnindexedString() string {
|
||||
return ii.ifdPath
|
||||
}
|
||||
|
||||
// IfdTag returns the tag struct behind this IFD.
|
||||
func (ii *IfdIdentity) IfdTag() IfdTag {
|
||||
return ii.ifdTag
|
||||
}
|
||||
|
||||
// TagId returns the tag-ID of the IFD.
|
||||
func (ii *IfdIdentity) TagId() uint16 {
|
||||
return ii.ifdTag.TagId()
|
||||
}
|
||||
|
||||
// LeafPathPart returns the last right-most path-part, which represents the
|
||||
// current IFD.
|
||||
func (ii *IfdIdentity) LeafPathPart() IfdIdentityPart {
|
||||
return ii.parts[len(ii.parts)-1]
|
||||
}
|
||||
|
||||
// Name returns the simple name of this IFD.
|
||||
func (ii *IfdIdentity) Name() string {
|
||||
return ii.LeafPathPart().Name
|
||||
}
|
||||
|
||||
// Index returns the index of this IFD (more then one IFD under a parent IFD
|
||||
// will be numbered [0..n]).
|
||||
func (ii *IfdIdentity) Index() int {
|
||||
return ii.LeafPathPart().Index
|
||||
}
|
||||
|
||||
// Equals returns true if the two IfdIdentity instances are effectively
|
||||
// identical.
|
||||
//
|
||||
// Since there's no way to get a specific fully-qualified IFD path without a
|
||||
// certain slice of parts and all other fields are also derived from this,
|
||||
// checking that the fully-qualified IFD path is equals is sufficient.
|
||||
func (ii *IfdIdentity) Equals(ii2 *IfdIdentity) bool {
|
||||
return ii.String() == ii2.String()
|
||||
}
|
||||
|
||||
// NewChild creates an IfdIdentity for an IFD that is a child of the current
|
||||
// IFD.
|
||||
func (ii *IfdIdentity) NewChild(childIfdTag IfdTag, index int) (iiChild *IfdIdentity) {
|
||||
if *childIfdTag.parentIfdTag != ii.ifdTag {
|
||||
log.Panicf("can not add child; we are not the parent:\nUS=%v\nCHILD=%v", ii.ifdTag, childIfdTag)
|
||||
}
|
||||
|
||||
childPart := IfdIdentityPart{childIfdTag.name, index}
|
||||
childParts := append(ii.parts, childPart)
|
||||
|
||||
iiChild = NewIfdIdentity(childIfdTag, childParts...)
|
||||
return iiChild
|
||||
}
|
||||
|
||||
// NewSibling creates an IfdIdentity for an IFD that is a sibling to the current
|
||||
// one.
|
||||
func (ii *IfdIdentity) NewSibling(index int) (iiSibling *IfdIdentity) {
|
||||
parts := make([]IfdIdentityPart, len(ii.parts))
|
||||
|
||||
copy(parts, ii.parts)
|
||||
parts[len(parts)-1].Index = index
|
||||
|
||||
iiSibling = NewIfdIdentity(ii.ifdTag, parts...)
|
||||
return iiSibling
|
||||
}
|
||||
|
||||
var (
|
||||
// IfdStandardIfdIdentity represents the IFD path for IFD0.
|
||||
IfdStandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 0})
|
||||
|
||||
// IfdExifStandardIfdIdentity represents the IFD path for IFD0/Exif0.
|
||||
IfdExifStandardIfdIdentity = IfdStandardIfdIdentity.NewChild(exifStandardIfd, 0)
|
||||
|
||||
// IfdExifIopStandardIfdIdentity represents the IFD path for IFD0/Exif0/Iop0.
|
||||
IfdExifIopStandardIfdIdentity = IfdExifStandardIfdIdentity.NewChild(iopStandardIfd, 0)
|
||||
|
||||
// IfdGPSInfoStandardIfdIdentity represents the IFD path for IFD0/GPSInfo0.
|
||||
IfdGpsInfoStandardIfdIdentity = IfdStandardIfdIdentity.NewChild(gpsInfoStandardIfd, 0)
|
||||
|
||||
// Ifd1StandardIfdIdentity represents the IFD path for IFD1.
|
||||
Ifd1StandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 1})
|
||||
)
|
||||
|
||||
var (
|
||||
// RELEASE(dustin): These are for backwards-compatibility. These used to be strings but are now IfdIdentity structs and the newer "StandardIfdIdentity" symbols above should be used instead. These will be removed in the next release.
|
||||
|
||||
IfdPathStandard = IfdStandardIfdIdentity
|
||||
IfdPathStandardExif = IfdExifStandardIfdIdentity
|
||||
IfdPathStandardExifIop = IfdExifIopStandardIfdIdentity
|
||||
IfdPathStandardGps = IfdGpsInfoStandardIfdIdentity
|
||||
)
|
|
@ -0,0 +1,334 @@
|
|||
package exifcommon
|
||||
|
||||
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{
|
||||
IfdStandardIfdIdentity.TagId(),
|
||||
IfdExifStandardIfdIdentity.TagId(),
|
||||
IfdExifIopStandardIfdIdentity.TagId(),
|
||||
})
|
||||
|
||||
log.PanicIf(err)
|
||||
|
||||
if mi.ParentTagId != IfdExifStandardIfdIdentity.TagId() {
|
||||
t.Fatalf("Parent tag-ID not correct")
|
||||
} else if mi.TagId != IfdExifIopStandardIfdIdentity.TagId() {
|
||||
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 != IfdExifStandardIfdIdentity.TagId() {
|
||||
t.Fatalf("Parent tag-ID not correct")
|
||||
} else if mi.TagId != IfdExifIopStandardIfdIdentity.TagId() {
|
||||
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{
|
||||
{Name: "IFD", TagId: 0, Index: 0},
|
||||
{Name: "Exif", TagId: 0x8769, Index: 0},
|
||||
{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{
|
||||
{Name: "IFD", TagId: 0, Index: 0},
|
||||
{Name: "Exif", TagId: 0x8769, Index: 1},
|
||||
{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{
|
||||
{Name: "IFD", Index: 0},
|
||||
{Name: "Exif", Index: 1},
|
||||
{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{
|
||||
{Name: "IFD", Index: 0},
|
||||
{Name: "Exif", Index: 1},
|
||||
{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)
|
||||
log.PanicIf(err)
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIfdIdentityFromString_Valid_WithoutIndexes(t *testing.T) {
|
||||
im := NewIfdMapping()
|
||||
|
||||
err := LoadStandardIfds(im)
|
||||
log.PanicIf(err)
|
||||
|
||||
fqIfdPath := "IFD/Exif"
|
||||
|
||||
ii, err := NewIfdIdentityFromString(im, fqIfdPath)
|
||||
log.PanicIf(err)
|
||||
|
||||
if ii.String() != fqIfdPath {
|
||||
t.Fatalf("'%s' IFD-path was not parsed correctly: [%s]", fqIfdPath, ii.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIfdIdentityFromString_Valid_WithIndexes(t *testing.T) {
|
||||
im := NewIfdMapping()
|
||||
|
||||
err := LoadStandardIfds(im)
|
||||
log.PanicIf(err)
|
||||
|
||||
fqIfdPath := "IFD2/Exif4"
|
||||
|
||||
ii, err := NewIfdIdentityFromString(im, fqIfdPath)
|
||||
log.PanicIf(err)
|
||||
|
||||
if ii.String() != fqIfdPath {
|
||||
t.Fatalf("'%s' IFD-path was not parsed correctly: [%s]", fqIfdPath, ii.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIfdIdentityFromString_Invalid_IfdPathJustRoot(t *testing.T) {
|
||||
im := NewIfdMapping()
|
||||
|
||||
err := LoadStandardIfds(im)
|
||||
log.PanicIf(err)
|
||||
|
||||
fqIfdPath := "XYZ"
|
||||
|
||||
_, err = NewIfdIdentityFromString(im, fqIfdPath)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error from invalid path.")
|
||||
} else if err.Error() != "ifd child with name [XYZ] not registered: [XYZ]" {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIfdIdentityFromString_Invalid_IfdPathWithSubdirectory(t *testing.T) {
|
||||
im := NewIfdMapping()
|
||||
|
||||
err := LoadStandardIfds(im)
|
||||
log.PanicIf(err)
|
||||
|
||||
fqIfdPath := "IFD/XYZ"
|
||||
|
||||
_, err = NewIfdIdentityFromString(im, fqIfdPath)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error from invalid path.")
|
||||
} else if err.Error() != "ifd child with name [XYZ] not registered: [IFD/XYZ]" {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
parserLogger = log.NewLogger("exifcommon.parser")
|
||||
)
|
||||
|
||||
// Parser knows how to parse all well-defined, encoded EXIF types.
|
||||
type Parser struct {
|
||||
}
|
||||
|
||||
// ParseBytesknows how to parse a byte-type value.
|
||||
func (p *Parser) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
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 that
|
||||
// should be at the end of the encoding.
|
||||
func (p *Parser) ParseAscii(data []byte, unitCount uint32) (value string, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
count := int(unitCount)
|
||||
|
||||
if len(data) < (TypeAscii.Size() * count) {
|
||||
log.Panic(ErrNotEnoughData)
|
||||
}
|
||||
|
||||
if len(data) == 0 || data[count-1] != 0 {
|
||||
s := string(data[:count])
|
||||
parserLogger.Warningf(nil, "ascii not terminated with nul as expected: [%v]", s)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
count := int(unitCount)
|
||||
|
||||
if len(data) < (TypeAscii.Size() * count) {
|
||||
log.Panic(ErrNotEnoughData)
|
||||
}
|
||||
|
||||
return string(data[:count]), nil
|
||||
}
|
||||
|
||||
// ParseShorts knows how to parse an encoded list of shorts.
|
||||
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))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ParseLongs knows how to encode an encoded list of unsigned longs.
|
||||
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))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ParseRationals knows how to parse an encoded list of unsigned rationals.
|
||||
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))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ParseSignedLongs knows how to parse an encoded list of signed longs.
|
||||
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))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ParseSignedRationals knows how to parse an encoded list of signed
|
||||
// rationals.
|
||||
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))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
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,320 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
func TestParser_ParseBytes(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte("abcdefg")
|
||||
|
||||
value, err := p.ParseBytes(encoded, 1)
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(value, encoded[:1]) != true {
|
||||
t.Fatalf("Encoding not correct (1): %v", value)
|
||||
}
|
||||
|
||||
value, err = p.ParseBytes(encoded, 4)
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(value, encoded[:4]) != true {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
|
||||
value, err = p.ParseBytes(encoded, uint32(len(encoded)))
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(value, encoded) != true {
|
||||
t.Fatalf("Encoding not correct (3): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseAscii(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
original := "abcdefg"
|
||||
|
||||
encoded := []byte(original[:1])
|
||||
encoded = append(encoded, 0)
|
||||
|
||||
value, err := p.ParseAscii(encoded, uint32(len(encoded)))
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != original[:1] {
|
||||
t.Fatalf("Encoding not correct (1): %s", value)
|
||||
}
|
||||
|
||||
encoded = []byte(original[:4])
|
||||
encoded = append(encoded, 0)
|
||||
|
||||
value, err = p.ParseAscii(encoded, uint32(len(encoded)))
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != original[:4] {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
|
||||
encoded = []byte(original)
|
||||
encoded = append(encoded, 0)
|
||||
|
||||
value, err = p.ParseAscii(encoded, uint32(len(encoded)))
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != original {
|
||||
t.Fatalf("Encoding not correct (3): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseAsciiNoNul(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
original := "abcdefg"
|
||||
|
||||
encoded := []byte(original)
|
||||
|
||||
value, err := p.ParseAsciiNoNul(encoded, 1)
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != original[:1] {
|
||||
t.Fatalf("Encoding not correct (1): %s", value)
|
||||
}
|
||||
|
||||
value, err = p.ParseAsciiNoNul(encoded, 4)
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != original[:4] {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
|
||||
value, err = p.ParseAsciiNoNul(encoded, uint32(len(encoded)))
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != original {
|
||||
t.Fatalf("Encoding not correct (3): (%d) %v", len(value), value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseShorts__Single(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{0x00, 0x01}
|
||||
|
||||
value, err := p.ParseShorts(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint16{1}) != true {
|
||||
t.Fatalf("Encoding not correct (1): %v", value)
|
||||
}
|
||||
|
||||
encoded = []byte{0x00, 0x01, 0x00, 0x02}
|
||||
|
||||
value, err = p.ParseShorts(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint16{1}) != true {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseShorts__Multiple(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{0x00, 0x01, 0x00, 0x02}
|
||||
|
||||
value, err := p.ParseShorts(encoded, 2, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint16{1, 2}) != true {
|
||||
t.Fatalf("Encoding not correct: %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseLongs__Single(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{0x00, 0x00, 0x00, 0x01}
|
||||
|
||||
value, err := p.ParseLongs(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint32{1}) != true {
|
||||
t.Fatalf("Encoding not correct (1): %v", value)
|
||||
}
|
||||
|
||||
encoded = []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}
|
||||
|
||||
value, err = p.ParseLongs(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint32{1}) != true {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseLongs__Multiple(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}
|
||||
|
||||
value, err := p.ParseLongs(encoded, 2, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint32{1, 2}) != true {
|
||||
t.Fatalf("Encoding not correct: %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseRationals__Single(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
|
||||
}
|
||||
|
||||
value, err := p.ParseRationals(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := []Rational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("Encoding not correct (1): %v", value)
|
||||
}
|
||||
|
||||
encoded = []byte{
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
|
||||
}
|
||||
|
||||
value, err = p.ParseRationals(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expected = []Rational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseRationals__Multiple(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
|
||||
}
|
||||
|
||||
value, err := p.ParseRationals(encoded, 2, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := []Rational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
{Numerator: 3, Denominator: 4},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseSignedLongs__Single(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{0x00, 0x00, 0x00, 0x01}
|
||||
|
||||
value, err := p.ParseSignedLongs(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []int32{1}) != true {
|
||||
t.Fatalf("Encoding not correct (1): %v", value)
|
||||
}
|
||||
|
||||
encoded = []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}
|
||||
|
||||
value, err = p.ParseSignedLongs(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []int32{1}) != true {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseSignedLongs__Multiple(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}
|
||||
|
||||
value, err := p.ParseSignedLongs(encoded, 2, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []int32{1, 2}) != true {
|
||||
t.Fatalf("Encoding not correct: %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseSignedRationals__Single(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
|
||||
}
|
||||
|
||||
value, err := p.ParseSignedRationals(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := []SignedRational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("Encoding not correct (1): %v", value)
|
||||
}
|
||||
|
||||
encoded = []byte{
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
|
||||
}
|
||||
|
||||
value, err = p.ParseSignedRationals(encoded, 1, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expected = []SignedRational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ParseSignedRationals__Multiple(t *testing.T) {
|
||||
p := new(Parser)
|
||||
|
||||
encoded := []byte{
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
|
||||
}
|
||||
|
||||
value, err := p.ParseSignedRationals(encoded, 2, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := []SignedRational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
{Numerator: 3, Denominator: 4},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("Encoding not correct (2): %v", value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
moduleRootPath = ""
|
||||
|
||||
testExifData []byte = nil
|
||||
|
||||
// EncodeDefaultByteOrder is the default byte-order for encoding operations.
|
||||
EncodeDefaultByteOrder = binary.BigEndian
|
||||
|
||||
// Default byte order for tests.
|
||||
TestDefaultByteOrder = binary.BigEndian
|
||||
)
|
||||
|
||||
func GetModuleRootPath() string {
|
||||
if moduleRootPath == "" {
|
||||
moduleRootPath = os.Getenv("EXIF_MODULE_ROOT_PATH")
|
||||
if moduleRootPath != "" {
|
||||
return moduleRootPath
|
||||
}
|
||||
|
||||
currentWd, err := os.Getwd()
|
||||
log.PanicIf(err)
|
||||
|
||||
currentPath := currentWd
|
||||
|
||||
visited := make([]string, 0)
|
||||
|
||||
for {
|
||||
tryStampFilepath := path.Join(currentPath, ".MODULE_ROOT")
|
||||
|
||||
_, err := os.Stat(tryStampFilepath)
|
||||
if err != nil && os.IsNotExist(err) != true {
|
||||
log.Panic(err)
|
||||
} else if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
visited = append(visited, tryStampFilepath)
|
||||
|
||||
currentPath = path.Dir(currentPath)
|
||||
if currentPath == "/" {
|
||||
log.Panicf("could not find module-root: %v", visited)
|
||||
}
|
||||
}
|
||||
|
||||
moduleRootPath = currentPath
|
||||
}
|
||||
|
||||
return moduleRootPath
|
||||
}
|
||||
|
||||
func GetTestAssetsPath() string {
|
||||
moduleRootPath := GetModuleRootPath()
|
||||
assetsPath := path.Join(moduleRootPath, "assets")
|
||||
|
||||
return assetsPath
|
||||
}
|
||||
|
||||
func getTestImageFilepath() string {
|
||||
assetsPath := GetTestAssetsPath()
|
||||
testImageFilepath := path.Join(assetsPath, "NDM_8901.jpg")
|
||||
return testImageFilepath
|
||||
}
|
||||
|
||||
func getTestExifData() []byte {
|
||||
if testExifData == nil {
|
||||
assetsPath := GetTestAssetsPath()
|
||||
filepath := path.Join(assetsPath, "NDM_8901.jpg.exif")
|
||||
|
||||
var err error
|
||||
|
||||
testExifData, err = ioutil.ReadFile(filepath)
|
||||
log.PanicIf(err)
|
||||
}
|
||||
|
||||
return testExifData
|
||||
}
|
|
@ -0,0 +1,452 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
typeLogger = log.NewLogger("exif.type")
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotEnoughData is used when there isn't enough data to accommodate 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")
|
||||
|
||||
// ErrUnhandledUndefinedTypedTag 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).
|
||||
ErrUnhandledUndefinedTypedTag = errors.New("not a standard unknown-typed tag")
|
||||
)
|
||||
|
||||
// TagTypePrimitive is a type-alias that let's us easily lookup type properties.
|
||||
type TagTypePrimitive uint16
|
||||
|
||||
const (
|
||||
// TypeByte describes an encoded list of bytes.
|
||||
TypeByte TagTypePrimitive = 1
|
||||
|
||||
// TypeAscii describes an encoded list of characters that is terminated
|
||||
// with a NUL in its encoded form.
|
||||
TypeAscii TagTypePrimitive = 2
|
||||
|
||||
// TypeShort describes an encoded list of shorts.
|
||||
TypeShort TagTypePrimitive = 3
|
||||
|
||||
// TypeLong describes an encoded list of longs.
|
||||
TypeLong TagTypePrimitive = 4
|
||||
|
||||
// TypeRational describes an encoded list of rationals.
|
||||
TypeRational TagTypePrimitive = 5
|
||||
|
||||
// TypeUndefined describes an encoded value that has a complex/non-clearcut
|
||||
// interpretation.
|
||||
TypeUndefined TagTypePrimitive = 7
|
||||
|
||||
// We've seen type-8, but have no documentation on it.
|
||||
|
||||
// TypeSignedLong describes an encoded list of signed longs.
|
||||
TypeSignedLong TagTypePrimitive = 9
|
||||
|
||||
// TypeSignedRational describes an encoded list of signed rationals.
|
||||
TypeSignedRational TagTypePrimitive = 10
|
||||
|
||||
// TypeAsciiNoNul is just a pseudo-type, for our own purposes.
|
||||
TypeAsciiNoNul TagTypePrimitive = 0xf0
|
||||
)
|
||||
|
||||
// String returns the name of the type
|
||||
func (typeType TagTypePrimitive) String() string {
|
||||
return TypeNames[typeType]
|
||||
}
|
||||
|
||||
// Size returns the size of one atomic unit of the type.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid returns true if tagType is a valid type.
|
||||
func (tagType TagTypePrimitive) IsValid() bool {
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
return tagType == TypeByte ||
|
||||
tagType == TypeAscii ||
|
||||
tagType == TypeAsciiNoNul ||
|
||||
tagType == TypeShort ||
|
||||
tagType == TypeLong ||
|
||||
tagType == TypeRational ||
|
||||
tagType == TypeSignedLong ||
|
||||
tagType == TypeSignedRational ||
|
||||
tagType == TypeUndefined
|
||||
}
|
||||
|
||||
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{}
|
||||
)
|
||||
|
||||
// Rational describes an unsigned rational value.
|
||||
type Rational struct {
|
||||
// Numerator is the numerator of the rational value.
|
||||
Numerator uint32
|
||||
|
||||
// Denominator is the numerator of the rational value.
|
||||
Denominator uint32
|
||||
}
|
||||
|
||||
// SignedRational describes a signed rational value.
|
||||
type SignedRational struct {
|
||||
// Numerator is the numerator of the rational value.
|
||||
Numerator int32
|
||||
|
||||
// Denominator is the numerator of the rational value.
|
||||
Denominator int32
|
||||
}
|
||||
|
||||
// Format returns a stringified value for the given encoding. Automatically
|
||||
// parses. Automatically calculates count based on type size. This function
|
||||
// also supports undefined-type values (the ones that we support, anyway) by
|
||||
// way of the String() method that they all require. We can't be more specific
|
||||
// because we're a base package and we can't refer to it.
|
||||
func FormatFromType(value interface{}, justFirst bool) (phrase string, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): !! Add test
|
||||
|
||||
switch t := value.(type) {
|
||||
case []byte:
|
||||
return DumpBytesToString(t), nil
|
||||
case string:
|
||||
return t, nil
|
||||
case []uint16:
|
||||
if len(t) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if justFirst == true {
|
||||
var valueSuffix string
|
||||
if len(t) > 1 {
|
||||
valueSuffix = "..."
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v%s", t[0], valueSuffix), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", t), nil
|
||||
case []uint32:
|
||||
if len(t) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if justFirst == true {
|
||||
var valueSuffix string
|
||||
if len(t) > 1 {
|
||||
valueSuffix = "..."
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v%s", t[0], valueSuffix), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", t), nil
|
||||
case []Rational:
|
||||
if len(t) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
parts := make([]string, len(t))
|
||||
for i, r := range t {
|
||||
parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
|
||||
|
||||
if justFirst == true {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if justFirst == true {
|
||||
var valueSuffix string
|
||||
if len(t) > 1 {
|
||||
valueSuffix = "..."
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", parts), nil
|
||||
case []int32:
|
||||
if len(t) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if justFirst == true {
|
||||
var valueSuffix string
|
||||
if len(t) > 1 {
|
||||
valueSuffix = "..."
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v%s", t[0], valueSuffix), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", t), nil
|
||||
case []SignedRational:
|
||||
if len(t) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
parts := make([]string, len(t))
|
||||
for i, r := range t {
|
||||
parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
|
||||
|
||||
if justFirst == true {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if justFirst == true {
|
||||
var valueSuffix string
|
||||
if len(t) > 1 {
|
||||
valueSuffix = "..."
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", parts), nil
|
||||
case fmt.Stringer:
|
||||
// An undefined value that is documented (or that we otherwise support).
|
||||
return t.String(), nil
|
||||
default:
|
||||
// Affects only "unknown" values, in general.
|
||||
log.Panicf("type can not be formatted into string: %v", reflect.TypeOf(value).Name())
|
||||
|
||||
// Never called.
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
// Format returns a stringified value for the given encoding. Automatically
|
||||
// parses. Automatically calculates count based on type size.
|
||||
func FormatFromBytes(rawBytes []byte, tagType TagTypePrimitive, justFirst bool, byteOrder binary.ByteOrder) (phrase string, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): !! Add test
|
||||
|
||||
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.
|
||||
|
||||
var value interface{}
|
||||
|
||||
switch tagType {
|
||||
case TypeByte:
|
||||
var err error
|
||||
|
||||
value, err = parser.ParseBytes(rawBytes, unitCount)
|
||||
log.PanicIf(err)
|
||||
case TypeAscii:
|
||||
var err error
|
||||
|
||||
value, err = parser.ParseAscii(rawBytes, unitCount)
|
||||
log.PanicIf(err)
|
||||
case TypeAsciiNoNul:
|
||||
var err error
|
||||
|
||||
value, err = parser.ParseAsciiNoNul(rawBytes, unitCount)
|
||||
log.PanicIf(err)
|
||||
case TypeShort:
|
||||
var err error
|
||||
|
||||
value, err = parser.ParseShorts(rawBytes, unitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
case TypeLong:
|
||||
var err error
|
||||
|
||||
value, err = parser.ParseLongs(rawBytes, unitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
case TypeRational:
|
||||
var err error
|
||||
|
||||
value, err = parser.ParseRationals(rawBytes, unitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
case TypeSignedLong:
|
||||
var err error
|
||||
|
||||
value, err = parser.ParseSignedLongs(rawBytes, unitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
case TypeSignedRational:
|
||||
var err error
|
||||
|
||||
value, err = parser.ParseSignedRationals(rawBytes, unitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
default:
|
||||
// Affects only "unknown" values, in general.
|
||||
log.Panicf("value of type [%s] can not be formatted into string", tagType.String())
|
||||
|
||||
// Never called.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
phrase, err = FormatFromType(value, justFirst)
|
||||
log.PanicIf(err)
|
||||
|
||||
return phrase, nil
|
||||
}
|
||||
|
||||
// TranslateStringToType converts user-provided strings to properly-typed
|
||||
// values. If a string, returns a string. Else, assumes that it's a single
|
||||
// number. If a list needs to be processed, it is the caller's responsibility to
|
||||
// split it (according to whichever convention has been established).
|
||||
func TranslateStringToType(tagType TagTypePrimitive, valueString string) (value interface{}, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
if tagType == TypeUndefined {
|
||||
// The caller should just call String() on the decoded type.
|
||||
log.Panicf("undefined-type values are not supported")
|
||||
}
|
||||
|
||||
if tagType == TypeByte {
|
||||
wide, err := strconv.ParseInt(valueString, 16, 8)
|
||||
log.PanicIf(err)
|
||||
|
||||
return byte(wide), 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
|
||||
}
|
||||
|
||||
// GetTypeByName returns the `TagTypePrimitive` for the given type name.
|
||||
// Returns (0) if not valid.
|
||||
func GetTypeByName(typeName string) (tagType TagTypePrimitive, found bool) {
|
||||
tagType, found = typeNamesR[typeName]
|
||||
return tagType, found
|
||||
}
|
||||
|
||||
// BasicTag describes a single tag for any purpose.
|
||||
type BasicTag struct {
|
||||
// FqIfdPath is the fully-qualified IFD-path.
|
||||
FqIfdPath string
|
||||
|
||||
// IfdPath is the unindexed IFD-path.
|
||||
IfdPath string
|
||||
|
||||
// TagId is the tag-ID.
|
||||
TagId uint16
|
||||
}
|
||||
|
||||
func init() {
|
||||
for typeId, typeName := range TypeNames {
|
||||
typeNamesR[typeName] = typeId
|
||||
}
|
||||
}
|
|
@ -0,0 +1,344 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
func TestTypeByte_String(t *testing.T) {
|
||||
if TypeByte.String() != "BYTE" {
|
||||
t.Fatalf("Type name not correct (byte): [%s]", TypeByte.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeAscii_String(t *testing.T) {
|
||||
if TypeAscii.String() != "ASCII" {
|
||||
t.Fatalf("Type name not correct (ASCII): [%s]", TypeAscii.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeAsciiNoNul_String(t *testing.T) {
|
||||
if TypeAsciiNoNul.String() != "_ASCII_NO_NUL" {
|
||||
t.Fatalf("Type name not correct (ASCII no-NUL): [%s]", TypeAsciiNoNul.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeShort_String(t *testing.T) {
|
||||
if TypeShort.String() != "SHORT" {
|
||||
t.Fatalf("Type name not correct (short): [%s]", TypeShort.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeLong_String(t *testing.T) {
|
||||
if TypeLong.String() != "LONG" {
|
||||
t.Fatalf("Type name not correct (long): [%s]", TypeLong.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeRational_String(t *testing.T) {
|
||||
if TypeRational.String() != "RATIONAL" {
|
||||
t.Fatalf("Type name not correct (rational): [%s]", TypeRational.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeSignedLong_String(t *testing.T) {
|
||||
if TypeSignedLong.String() != "SLONG" {
|
||||
t.Fatalf("Type name not correct (signed long): [%s]", TypeSignedLong.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeSignedRational_String(t *testing.T) {
|
||||
if TypeSignedRational.String() != "SRATIONAL" {
|
||||
t.Fatalf("Type name not correct (signed rational): [%s]", TypeSignedRational.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeByte_Size(t *testing.T) {
|
||||
if TypeByte.Size() != 1 {
|
||||
t.Fatalf("Type size not correct (byte): (%d)", TypeByte.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeAscii_Size(t *testing.T) {
|
||||
if TypeAscii.Size() != 1 {
|
||||
t.Fatalf("Type size not correct (ASCII): (%d)", TypeAscii.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeAsciiNoNul_Size(t *testing.T) {
|
||||
if TypeAsciiNoNul.Size() != 1 {
|
||||
t.Fatalf("Type size not correct (ASCII no-NUL): (%d)", TypeAsciiNoNul.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeShort_Size(t *testing.T) {
|
||||
if TypeShort.Size() != 2 {
|
||||
t.Fatalf("Type size not correct (short): (%d)", TypeShort.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeLong_Size(t *testing.T) {
|
||||
if TypeLong.Size() != 4 {
|
||||
t.Fatalf("Type size not correct (long): (%d)", TypeLong.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeRational_Size(t *testing.T) {
|
||||
if TypeRational.Size() != 8 {
|
||||
t.Fatalf("Type size not correct (rational): (%d)", TypeRational.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeSignedLong_Size(t *testing.T) {
|
||||
if TypeSignedLong.Size() != 4 {
|
||||
t.Fatalf("Type size not correct (signed long): (%d)", TypeSignedLong.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeSignedRational_Size(t *testing.T) {
|
||||
if TypeSignedRational.Size() != 8 {
|
||||
t.Fatalf("Type size not correct (signed rational): (%d)", TypeSignedRational.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat__Byte(t *testing.T) {
|
||||
r := []byte{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
|
||||
s, err := FormatFromBytes(r, TypeByte, false, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if s != "01 02 03 04 05 06 07 08" {
|
||||
t.Fatalf("Format output not correct (bytes): [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat__Ascii(t *testing.T) {
|
||||
r := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0}
|
||||
|
||||
s, err := FormatFromBytes(r, TypeAscii, false, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if s != "abcdefg" {
|
||||
t.Fatalf("Format output not correct (ASCII): [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat__AsciiNoNul(t *testing.T) {
|
||||
r := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
|
||||
|
||||
s, err := FormatFromBytes(r, TypeAsciiNoNul, false, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if s != "abcdefgh" {
|
||||
t.Fatalf("Format output not correct (ASCII no-NUL): [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat__Short(t *testing.T) {
|
||||
r := []byte{0, 1, 0, 2}
|
||||
|
||||
s, err := FormatFromBytes(r, TypeShort, false, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if s != "[1 2]" {
|
||||
t.Fatalf("Format output not correct (shorts): [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat__Long(t *testing.T) {
|
||||
r := []byte{0, 0, 0, 1, 0, 0, 0, 2}
|
||||
|
||||
s, err := FormatFromBytes(r, TypeLong, false, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if s != "[1 2]" {
|
||||
t.Fatalf("Format output not correct (longs): [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat__Rational(t *testing.T) {
|
||||
r := []byte{
|
||||
0, 0, 0, 1, 0, 0, 0, 2,
|
||||
0, 0, 0, 3, 0, 0, 0, 4,
|
||||
}
|
||||
|
||||
s, err := FormatFromBytes(r, TypeRational, false, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if s != "[1/2 3/4]" {
|
||||
t.Fatalf("Format output not correct (rationals): [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat__SignedLong(t *testing.T) {
|
||||
r := []byte{0, 0, 0, 1, 0, 0, 0, 2}
|
||||
|
||||
s, err := FormatFromBytes(r, TypeSignedLong, false, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if s != "[1 2]" {
|
||||
t.Fatalf("Format output not correct (signed longs): [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat__SignedRational(t *testing.T) {
|
||||
r := []byte{
|
||||
0, 0, 0, 1, 0, 0, 0, 2,
|
||||
0, 0, 0, 3, 0, 0, 0, 4,
|
||||
}
|
||||
|
||||
s, err := FormatFromBytes(r, TypeSignedRational, false, TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if s != "[1/2 3/4]" {
|
||||
t.Fatalf("Format output not correct (signed rationals): [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat__Undefined(t *testing.T) {
|
||||
r := []byte{'a', 'b'}
|
||||
|
||||
_, err := FormatFromBytes(r, TypeUndefined, false, TestDefaultByteOrder)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error.")
|
||||
} else if err.Error() != "can not determine tag-value size for type (7): [UNDEFINED]" {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__TypeUndefined(t *testing.T) {
|
||||
_, err := TranslateStringToType(TypeUndefined, "")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error.")
|
||||
} else if err.Error() != "undefined-type values are not supported" {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__TypeByte(t *testing.T) {
|
||||
v, err := TranslateStringToType(TypeByte, "02")
|
||||
log.PanicIf(err)
|
||||
|
||||
if v != byte(2) {
|
||||
t.Fatalf("Translation of string to type not correct (bytes): %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__TypeAscii(t *testing.T) {
|
||||
v, err := TranslateStringToType(TypeAscii, "abcdefgh")
|
||||
log.PanicIf(err)
|
||||
|
||||
if v != "abcdefgh" {
|
||||
t.Fatalf("Translation of string to type not correct (ascii): %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__TypeAsciiNoNul(t *testing.T) {
|
||||
v, err := TranslateStringToType(TypeAsciiNoNul, "abcdefgh")
|
||||
log.PanicIf(err)
|
||||
|
||||
if v != "abcdefgh" {
|
||||
t.Fatalf("Translation of string to type not correct (ascii no-NUL): %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__TypeShort(t *testing.T) {
|
||||
v, err := TranslateStringToType(TypeShort, "11")
|
||||
log.PanicIf(err)
|
||||
|
||||
if v != uint16(11) {
|
||||
t.Fatalf("Translation of string to type not correct (short): %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__TypeLong(t *testing.T) {
|
||||
v, err := TranslateStringToType(TypeLong, "11")
|
||||
log.PanicIf(err)
|
||||
|
||||
if v != uint32(11) {
|
||||
t.Fatalf("Translation of string to type not correct (long): %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__TypeRational(t *testing.T) {
|
||||
v, err := TranslateStringToType(TypeRational, "11/22")
|
||||
log.PanicIf(err)
|
||||
|
||||
r := v.(Rational)
|
||||
|
||||
if r.Numerator != 11 || r.Denominator != 22 {
|
||||
t.Fatalf("Translation of string to type not correct (rational): %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__TypeSignedLong(t *testing.T) {
|
||||
v, err := TranslateStringToType(TypeSignedLong, "11")
|
||||
log.PanicIf(err)
|
||||
|
||||
if v != int32(11) {
|
||||
t.Fatalf("Translation of string to type not correct (signed long): %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__TypeSignedRational(t *testing.T) {
|
||||
v, err := TranslateStringToType(TypeSignedRational, "11/22")
|
||||
log.PanicIf(err)
|
||||
|
||||
r := v.(SignedRational)
|
||||
|
||||
if r.Numerator != 11 || r.Denominator != 22 {
|
||||
t.Fatalf("Translation of string to type not correct (signed rational): %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateStringToType__InvalidType(t *testing.T) {
|
||||
_, err := TranslateStringToType(99, "11/22")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for invalid type.")
|
||||
} else if err.Error() != "from-string encoding for type not supported; this shouldn't happen: []" {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// } 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
|
||||
// }
|
|
@ -0,0 +1,82 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
// DumpBytes prints a list of hex-encoded bytes.
|
||||
func DumpBytes(data []byte) {
|
||||
fmt.Printf("DUMP: ")
|
||||
for _, x := range data {
|
||||
fmt.Printf("%02x ", x)
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
// DumpBytesClause prints a list like DumpBytes(), but encapsulated in
|
||||
// "[]byte { ... }".
|
||||
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")
|
||||
}
|
||||
|
||||
// DumpBytesToString returns a stringified list of hex-encoded bytes.
|
||||
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()
|
||||
}
|
||||
|
||||
// DumpBytesClauseToString returns a comma-separated list of hex-encoded bytes.
|
||||
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()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
// RELEASE(dustin): Dump this for the next release. It duplicates the same function now in exifcommon.
|
||||
|
||||
t = t.UTC()
|
||||
|
||||
return fmt.Sprintf("%04d:%02d:%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"testing"
|
||||
// "github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
func TestDumpBytes(t *testing.T) {
|
||||
DumpBytes([]byte{1, 2, 3, 4})
|
||||
}
|
||||
|
||||
func TestDumpBytesClause(t *testing.T) {
|
||||
DumpBytesClause([]byte{1, 2, 3, 4})
|
||||
}
|
||||
|
||||
func TestDumpBytesToString(t *testing.T) {
|
||||
s := DumpBytesToString([]byte{1, 2, 3, 4})
|
||||
if s != "01 02 03 04" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDumpBytesClauseToString(t *testing.T) {
|
||||
s := DumpBytesClauseToString([]byte{1, 2, 3, 4})
|
||||
if s != "0x01, 0x02, 0x03, 0x04" {
|
||||
t.Fatalf("Stringified clause is not correct: [%s]", s)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,412 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
parser *Parser
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFarValue indicates that an offset-based lookup was attempted for a
|
||||
// non-offset-based (embedded) value.
|
||||
ErrNotFarValue = errors.New("not a far value")
|
||||
)
|
||||
|
||||
// ValueContext embeds 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
|
||||
}
|
||||
|
||||
// TODO(dustin): We can update newValueContext() to derive `valueOffset` itself (from `rawValueOffset`).
|
||||
|
||||
// NewValueContext returns a new ValueContext struct.
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// SetUndefinedValueType sets the effective type if this is an unknown-type tag.
|
||||
func (vc *ValueContext) SetUndefinedValueType(tagType TagTypePrimitive) {
|
||||
if vc.tagType != TypeUndefined {
|
||||
log.Panicf("can not set effective type for unknown-type tag because this is *not* an unknown-type tag")
|
||||
}
|
||||
|
||||
vc.undefinedValueTagType = tagType
|
||||
}
|
||||
|
||||
// UnitCount returns the embedded unit-count.
|
||||
func (vc *ValueContext) UnitCount() uint32 {
|
||||
return vc.unitCount
|
||||
}
|
||||
|
||||
// ValueOffset returns the value-offset decoded as a `uint32`.
|
||||
func (vc *ValueContext) ValueOffset() uint32 {
|
||||
return vc.valueOffset
|
||||
}
|
||||
|
||||
// RawValueOffset returns the uninterpreted value-offset. This is used for
|
||||
// embedded values (values small enough to fit within the offset bytes rather
|
||||
// than needing to be stored elsewhere and referred to by an actual offset).
|
||||
func (vc *ValueContext) RawValueOffset() []byte {
|
||||
return vc.rawValueOffset
|
||||
}
|
||||
|
||||
// AddressableData returns the block of data that we can dereference into.
|
||||
func (vc *ValueContext) AddressableData() []byte {
|
||||
return vc.addressableData
|
||||
}
|
||||
|
||||
// ByteOrder returns the byte-order of numbers.
|
||||
func (vc *ValueContext) ByteOrder() binary.ByteOrder {
|
||||
return vc.byteOrder
|
||||
}
|
||||
|
||||
// IfdPath returns the path of the IFD containing this tag.
|
||||
func (vc *ValueContext) IfdPath() string {
|
||||
return vc.ifdPath
|
||||
}
|
||||
|
||||
// TagId returns the ID of the tag that we represent.
|
||||
func (vc *ValueContext) TagId() uint16 {
|
||||
return vc.tagId
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// SizeInBytes returns the number of bytes that this value requires. The
|
||||
// underlying call will panic if the type is UNDEFINED. It is the
|
||||
// responsibility of the caller to preemptively check that.
|
||||
func (vc *ValueContext) SizeInBytes() int {
|
||||
tagType := vc.effectiveValueType()
|
||||
|
||||
return tagType.Size() * int(vc.unitCount)
|
||||
}
|
||||
|
||||
// effectiveValueType returns the effective type of the unknown-type tag or, if
|
||||
// not unknown, the actual type.
|
||||
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
|
||||
}
|
||||
|
||||
// readRawEncoded returns the encoded bytes for the value that we represent.
|
||||
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
|
||||
}
|
||||
|
||||
return vc.addressableData[vc.valueOffset : vc.valueOffset+vc.unitCount*unitSizeRaw], nil
|
||||
}
|
||||
|
||||
// GetFarOffset returns the offset if the value is not embedded [within the
|
||||
// pointer itself] or an error if an embedded value.
|
||||
func (vc *ValueContext) GetFarOffset() (offset uint32, err error) {
|
||||
if vc.isEmbedded() == true {
|
||||
return 0, ErrNotFarValue
|
||||
}
|
||||
|
||||
return vc.valueOffset, nil
|
||||
}
|
||||
|
||||
// ReadRawEncoded returns the encoded bytes for the value that we represent.
|
||||
func (vc *ValueContext) ReadRawEncoded() (rawBytes []byte, err error) {
|
||||
|
||||
// TODO(dustin): Remove this method and rename readRawEncoded in its place.
|
||||
|
||||
return vc.readRawEncoded()
|
||||
}
|
||||
|
||||
// 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 := FormatFromBytes(rawBytes, vc.effectiveValueType(), false, vc.byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
return phrase, nil
|
||||
}
|
||||
|
||||
// FormatFirst 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 := FormatFromBytes(rawBytes, vc.tagType, true, vc.byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
return phrase, nil
|
||||
}
|
||||
|
||||
// ReadBytes parses the encoded byte-array from the value-context.
|
||||
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
|
||||
}
|
||||
|
||||
// ReadAscii parses the encoded NUL-terminated ASCII string from the value-
|
||||
// context.
|
||||
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
|
||||
}
|
||||
|
||||
// ReadAsciiNoNul parses the non-NUL-terminated encoded ASCII string from the
|
||||
// value-context.
|
||||
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
|
||||
}
|
||||
|
||||
// ReadShorts parses the list of encoded shorts from the value-context.
|
||||
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
|
||||
}
|
||||
|
||||
// ReadLongs parses the list of encoded, unsigned longs from the value-context.
|
||||
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
|
||||
}
|
||||
|
||||
// ReadRationals parses the list of encoded, unsigned rationals from the value-
|
||||
// context.
|
||||
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
|
||||
}
|
||||
|
||||
// ReadSignedLongs parses the list of encoded, signed longs from the value-context.
|
||||
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
|
||||
}
|
||||
|
||||
// ReadSignedRationals parses the list of encoded, signed rationals from the
|
||||
// value-context.
|
||||
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
|
||||
}
|
||||
|
||||
func init() {
|
||||
parser = new(Parser)
|
||||
}
|
|
@ -0,0 +1,858 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
func TestNewValueContext(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeLong, TestDefaultByteOrder)
|
||||
|
||||
if vc.ifdPath != "aa/bb" {
|
||||
t.Fatalf("ifdPath not correct: [%s]", vc.ifdPath)
|
||||
} else if vc.tagId != 0x1234 {
|
||||
t.Fatalf("tagId not correct: (0x%04x)", vc.tagId)
|
||||
} else if vc.unitCount != 11 {
|
||||
t.Fatalf("unitCount not correct: (%d)", vc.unitCount)
|
||||
} else if vc.valueOffset != 22 {
|
||||
t.Fatalf("valueOffset not correct: (%d)", vc.valueOffset)
|
||||
} else if bytes.Equal(vc.rawValueOffset, rawValueOffset) != true {
|
||||
t.Fatalf("rawValueOffset not correct: %v", vc.rawValueOffset)
|
||||
} else if bytes.Equal(vc.addressableData, addressableData) != true {
|
||||
t.Fatalf("addressableData not correct: %v", vc.addressableData)
|
||||
} else if vc.tagType != TypeLong {
|
||||
t.Fatalf("tagType not correct: (%d)", vc.tagType)
|
||||
} else if vc.byteOrder != TestDefaultByteOrder {
|
||||
t.Fatalf("byteOrder not correct: %v", vc.byteOrder)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_SetUndefinedValueType__ErrorWhenNotUndefined(t *testing.T) {
|
||||
defer func() {
|
||||
if errRaw := recover(); errRaw != nil {
|
||||
err := errRaw.(error)
|
||||
if err.Error() != "can not set effective type for unknown-type tag because this is *not* an unknown-type tag" {
|
||||
t.Fatalf("Error not expected: [%s]", err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Fatalf("Expected error.")
|
||||
}()
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeLong, TestDefaultByteOrder)
|
||||
|
||||
vc.SetUndefinedValueType(TypeLong)
|
||||
}
|
||||
|
||||
func TestValueContext_SetUndefinedValueType__Ok(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
vc.SetUndefinedValueType(TypeLong)
|
||||
|
||||
if vc.tagType != TypeUndefined {
|
||||
t.Fatalf("Internal type not still 'undefined': (%d)", vc.tagType)
|
||||
} else if vc.undefinedValueTagType != TypeLong {
|
||||
t.Fatalf("Internal undefined-type not correct: (%d)", vc.undefinedValueTagType)
|
||||
} else if vc.effectiveValueType() != TypeLong {
|
||||
t.Fatalf("Effective tag not correct: (%d)", vc.effectiveValueType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_effectiveValueType(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
vc.SetUndefinedValueType(TypeLong)
|
||||
|
||||
if vc.tagType != TypeUndefined {
|
||||
t.Fatalf("Internal type not still 'undefined': (%d)", vc.tagType)
|
||||
} else if vc.undefinedValueTagType != TypeLong {
|
||||
t.Fatalf("Internal undefined-type not correct: (%d)", vc.undefinedValueTagType)
|
||||
} else if vc.effectiveValueType() != TypeLong {
|
||||
t.Fatalf("Effective tag not correct: (%d)", vc.effectiveValueType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_UnitCount(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
if vc.UnitCount() != 11 {
|
||||
t.Fatalf("UnitCount() not correct: (%d)", vc.UnitCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ValueOffset(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
if vc.ValueOffset() != 22 {
|
||||
t.Fatalf("ValueOffset() not correct: (%d)", vc.ValueOffset())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_RawValueOffset(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
if bytes.Equal(vc.RawValueOffset(), rawValueOffset) != true {
|
||||
t.Fatalf("RawValueOffset() not correct: %v", vc.RawValueOffset())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_AddressableData(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
if bytes.Equal(vc.AddressableData(), addressableData) != true {
|
||||
t.Fatalf("AddressableData() not correct: %v", vc.AddressableData())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ByteOrder(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
if vc.ByteOrder() != TestDefaultByteOrder {
|
||||
t.Fatalf("ByteOrder() not correct: %v", vc.ByteOrder())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_IfdPath(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
if vc.IfdPath() != "aa/bb" {
|
||||
t.Fatalf("IfdPath() not correct: [%s]", vc.IfdPath())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_TagId(t *testing.T) {
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, 11, 22, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
if vc.TagId() != 0x1234 {
|
||||
t.Fatalf("TagId() not correct: (%d)", vc.TagId())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_isEmbedded__True(t *testing.T) {
|
||||
unitCount := uint32(4)
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, 22, rawValueOffset, addressableData, TypeByte, TestDefaultByteOrder)
|
||||
|
||||
if vc.isEmbedded() != true {
|
||||
t.Fatalf("isEmbedded() not correct: %v", vc.isEmbedded())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_isEmbedded__False(t *testing.T) {
|
||||
unitCount := uint32(5)
|
||||
rawValueOffset := []byte{0, 0, 0, 22}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, 22, rawValueOffset, addressableData, TypeByte, TestDefaultByteOrder)
|
||||
|
||||
if vc.isEmbedded() != false {
|
||||
t.Fatalf("isEmbedded() not correct: %v", vc.isEmbedded())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_readRawEncoded__IsEmbedded(t *testing.T) {
|
||||
unitCount := uint32(4)
|
||||
|
||||
rawValueOffset := []byte{1, 2, 3, 4}
|
||||
|
||||
// Ignored, in this case.
|
||||
valueOffset := uint32(0)
|
||||
|
||||
addressableData := []byte{}
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeByte, TestDefaultByteOrder)
|
||||
|
||||
recovered, err := vc.readRawEncoded()
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(recovered, rawValueOffset) != true {
|
||||
t.Fatalf("Embedded value bytes not recovered correctly: %v", recovered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_readRawEncoded__IsRelative(t *testing.T) {
|
||||
unitCount := uint32(5)
|
||||
|
||||
// Ignored, in this case.
|
||||
rawValueOffset := []byte{0, 0, 0, 0}
|
||||
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{5, 6, 7, 8, 9}
|
||||
addressableData := []byte{1, 2, 3, 4}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeByte, TestDefaultByteOrder)
|
||||
|
||||
recovered, err := vc.readRawEncoded()
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(recovered, data) != true {
|
||||
t.Fatalf("Relative value bytes not recovered correctly: %v", recovered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__Byte(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeByte, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "61 62 63 64 65 66 67 68" {
|
||||
t.Fatalf("Format not correct for bytes: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__Ascii(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeAscii, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "abcdefg" {
|
||||
t.Fatalf("Format not correct for ASCII: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__AsciiNoNul(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeAsciiNoNul, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "abcdefgh" {
|
||||
t.Fatalf("Format not correct for ASCII (no NUL): [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__Short(t *testing.T) {
|
||||
unitCount := uint32(4)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 1, 0, 2, 0, 3, 0, 4}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeShort, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "[1 2 3 4]" {
|
||||
t.Fatalf("Format not correct for shorts: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__Long(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 0, 0, 1, 0, 0, 0, 2}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeLong, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "[1 2]" {
|
||||
t.Fatalf("Format not correct for longs: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__Rational(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{
|
||||
0, 0, 0, 1, 0, 0, 0, 2,
|
||||
0, 0, 0, 3, 0, 0, 0, 4,
|
||||
}
|
||||
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeRational, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "[1/2 3/4]" {
|
||||
t.Fatalf("Format not correct for rationals: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__SignedLong(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 0, 0, 1, 0, 0, 0, 2}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeSignedLong, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "[1 2]" {
|
||||
t.Fatalf("Format not correct for signed-longs: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__SignedRational(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{
|
||||
0, 0, 0, 1, 0, 0, 0, 2,
|
||||
0, 0, 0, 3, 0, 0, 0, 4,
|
||||
}
|
||||
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeSignedRational, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "[1/2 3/4]" {
|
||||
t.Fatalf("Format not correct for signed-rationals: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__Undefined__NoEffectiveType(t *testing.T) {
|
||||
defer func() {
|
||||
if errRaw := recover(); errRaw != nil {
|
||||
err := errRaw.(error)
|
||||
if err.Error() != "undefined-value type not set" {
|
||||
t.Fatalf("Error not expected: [%s]", err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Fatalf("Expected error.")
|
||||
}()
|
||||
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "61 62 63 64 65 66 67 68" {
|
||||
t.Fatalf("Format not correct for bytes: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Format__Undefined__HasEffectiveType(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeUndefined, TestDefaultByteOrder)
|
||||
|
||||
vc.SetUndefinedValueType(TypeAscii)
|
||||
|
||||
value, err := vc.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "abcdefg" {
|
||||
t.Fatalf("Format not correct for undefined (with effective type of string): [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_FormatFirst__Bytes(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeByte, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.FormatFirst()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "61 62 63 64 65 66 67 68" {
|
||||
t.Fatalf("FormatFirst not correct for bytes: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_FormatFirst__String(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeAscii, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.FormatFirst()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "abcdefg" {
|
||||
t.Fatalf("FormatFirst not correct for ASCII: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_FormatFirst__List(t *testing.T) {
|
||||
unitCount := uint32(4)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 1, 0, 2, 0, 3, 0, 4}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeShort, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.FormatFirst()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "1..." {
|
||||
t.Fatalf("FormatFirst not correct for shorts: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ReadBytes(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeByte, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.ReadBytes()
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(value, data) != true {
|
||||
t.Fatalf("ReadBytes not correct: %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ReadAscii(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeAscii, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.ReadAscii()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "abcdefg" {
|
||||
t.Fatalf("ReadAscii not correct: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ReadAsciiNoNul(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeAsciiNoNul, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.ReadAsciiNoNul()
|
||||
log.PanicIf(err)
|
||||
|
||||
if value != "abcdefgh" {
|
||||
t.Fatalf("ReadAsciiNoNul not correct: [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ReadShorts(t *testing.T) {
|
||||
unitCount := uint32(4)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 1, 0, 2, 0, 3, 0, 4}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeShort, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.ReadShorts()
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint16{1, 2, 3, 4}) != true {
|
||||
t.Fatalf("ReadShorts not correct: %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ReadLongs(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 0, 0, 1, 0, 0, 0, 2}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeLong, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.ReadLongs()
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint32{1, 2}) != true {
|
||||
t.Fatalf("ReadLongs not correct: %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ReadRationals(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{
|
||||
0, 0, 0, 1, 0, 0, 0, 2,
|
||||
0, 0, 0, 3, 0, 0, 0, 4,
|
||||
}
|
||||
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeRational, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.ReadRationals()
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := []Rational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
{Numerator: 3, Denominator: 4},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("ReadRationals not correct: %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ReadSignedLongs(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 0, 0, 1, 0, 0, 0, 2}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeSignedLong, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.ReadSignedLongs()
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []int32{1, 2}) != true {
|
||||
t.Fatalf("ReadSignedLongs not correct: %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_ReadSignedRationals(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{
|
||||
0, 0, 0, 1, 0, 0, 0, 2,
|
||||
0, 0, 0, 3, 0, 0, 0, 4,
|
||||
}
|
||||
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeSignedRational, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.ReadSignedRationals()
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := []SignedRational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
{Numerator: 3, Denominator: 4},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("ReadSignedRationals not correct: %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Values__Byte(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeByte, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Values()
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, data) != true {
|
||||
t.Fatalf("Values not correct (bytes): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Values__Ascii(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 0}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeAscii, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Values()
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, "abcdefg") != true {
|
||||
t.Fatalf("Values not correct (ASCII): [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Values__AsciiNoNul(t *testing.T) {
|
||||
unitCount := uint32(8)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeAsciiNoNul, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Values()
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, "abcdefgh") != true {
|
||||
t.Fatalf("Values not correct (ASCII no-NUL): [%s]", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Values__Short(t *testing.T) {
|
||||
unitCount := uint32(4)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 1, 0, 2, 0, 3, 0, 4}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeShort, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Values()
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint16{1, 2, 3, 4}) != true {
|
||||
t.Fatalf("Values not correct (shorts): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Values__Long(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 0, 0, 1, 0, 0, 0, 2}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeLong, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Values()
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []uint32{1, 2}) != true {
|
||||
t.Fatalf("Values not correct (longs): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Values__Rational(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{
|
||||
0, 0, 0, 1, 0, 0, 0, 2,
|
||||
0, 0, 0, 3, 0, 0, 0, 4,
|
||||
}
|
||||
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeRational, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Values()
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := []Rational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
{Numerator: 3, Denominator: 4},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("Values not correct (rationals): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Values__SignedLong(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{0, 0, 0, 1, 0, 0, 0, 2}
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeSignedLong, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Values()
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, []int32{1, 2}) != true {
|
||||
t.Fatalf("Values not correct (signed longs): %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueContext_Values__SignedRational(t *testing.T) {
|
||||
unitCount := uint32(2)
|
||||
|
||||
rawValueOffset := []byte{0, 0, 0, 4}
|
||||
valueOffset := uint32(4)
|
||||
|
||||
data := []byte{
|
||||
0, 0, 0, 1, 0, 0, 0, 2,
|
||||
0, 0, 0, 3, 0, 0, 0, 4,
|
||||
}
|
||||
|
||||
addressableData := []byte{0, 0, 0, 0}
|
||||
addressableData = append(addressableData, data...)
|
||||
|
||||
vc := NewValueContext("aa/bb", 0x1234, unitCount, valueOffset, rawValueOffset, addressableData, TypeSignedRational, TestDefaultByteOrder)
|
||||
|
||||
value, err := vc.Values()
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := []SignedRational{
|
||||
{Numerator: 1, Denominator: 2},
|
||||
{Numerator: 3, Denominator: 4},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expected) != true {
|
||||
t.Fatalf("Values not correct (signed rationals): %v", value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// ValueEncoder knows how to encode values of every type to bytes.
|
||||
type ValueEncoder struct {
|
||||
byteOrder binary.ByteOrder
|
||||
}
|
||||
|
||||
// NewValueEncoder returns a new ValueEncoder.
|
||||
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))
|
||||
}
|
||||
}()
|
||||
|
||||
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)
|
||||
case time.Time:
|
||||
// For convenience, if the user doesn't want to deal with translation
|
||||
// semantics with timestamps.
|
||||
|
||||
t := value.(time.Time)
|
||||
s := ExifFullTimestampString(t)
|
||||
|
||||
ed, err = ve.encodeAscii(s)
|
||||
log.PanicIf(err)
|
||||
default:
|
||||
log.Panicf("value not encodable: [%s] [%v]", reflect.TypeOf(value), value)
|
||||
}
|
||||
|
||||
return ed, nil
|
||||
}
|
|
@ -0,0 +1,592 @@
|
|||
package exifcommon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
func TestValueEncoder_encodeBytes__Cycle(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.")
|
||||
}
|
||||
|
||||
recovered, err := parser.ParseBytes(ed.Encoded, ed.UnitCount)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(recovered, original) != true {
|
||||
t.Fatalf("Value not recovered correctly.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEncoder_encodeAscii__Cycle(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.
|
||||
|
||||
recovered, err := parser.ParseAscii(ed.Encoded, ed.UnitCount)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(recovered, original) != true {
|
||||
t.Fatalf("Value not recovered correctly.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEncoder_encodeAsciiNoNul__Cycle(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).
|
||||
|
||||
recovered, err := parser.ParseAsciiNoNul(ed.Encoded, ed.UnitCount)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(recovered, string(expected)) != true {
|
||||
t.Fatalf("Value not recovered correctly.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEncoder_encodeShorts__Cycle(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.")
|
||||
}
|
||||
|
||||
recovered, err := parser.ParseShorts(ed.Encoded, ed.UnitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(recovered, original) != true {
|
||||
t.Fatalf("Value not recovered correctly.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEncoder_encodeLongs__Cycle(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.")
|
||||
}
|
||||
|
||||
recovered, err := parser.ParseLongs(ed.Encoded, ed.UnitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(recovered, original) != true {
|
||||
t.Fatalf("Value not recovered correctly.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEncoder_encodeRationals__Cycle(t *testing.T) {
|
||||
byteOrder := TestDefaultByteOrder
|
||||
ve := NewValueEncoder(byteOrder)
|
||||
|
||||
original := []Rational{
|
||||
{
|
||||
Numerator: 0x11,
|
||||
Denominator: 0x22,
|
||||
},
|
||||
{
|
||||
Numerator: 0x33,
|
||||
Denominator: 0x44,
|
||||
},
|
||||
{
|
||||
Numerator: 0x55,
|
||||
Denominator: 0x66,
|
||||
},
|
||||
{
|
||||
Numerator: 0x77,
|
||||
Denominator: 0x88,
|
||||
},
|
||||
{
|
||||
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.")
|
||||
}
|
||||
|
||||
recovered, err := parser.ParseRationals(ed.Encoded, ed.UnitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(recovered, original) != true {
|
||||
t.Fatalf("Value not recovered correctly.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEncoder_encodeSignedLongs__Cycle(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.")
|
||||
}
|
||||
|
||||
recovered, err := parser.ParseSignedLongs(ed.Encoded, ed.UnitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(recovered, original) != true {
|
||||
t.Fatalf("Value not recovered correctly.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEncoder_encodeSignedRationals__Cycle(t *testing.T) {
|
||||
byteOrder := TestDefaultByteOrder
|
||||
ve := NewValueEncoder(byteOrder)
|
||||
|
||||
original := []SignedRational{
|
||||
{
|
||||
Numerator: 0x11,
|
||||
Denominator: 0x22,
|
||||
},
|
||||
{
|
||||
Numerator: 0x33,
|
||||
Denominator: 0x44,
|
||||
},
|
||||
{
|
||||
Numerator: 0x55,
|
||||
Denominator: 0x66,
|
||||
},
|
||||
{
|
||||
Numerator: 0x77,
|
||||
Denominator: 0x88,
|
||||
},
|
||||
{
|
||||
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.")
|
||||
}
|
||||
|
||||
recovered, err := parser.ParseSignedRationals(ed.Encoded, ed.UnitCount, byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(recovered, original) != true {
|
||||
t.Fatalf("Value not recovered correctly.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEncoder_Encode__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 TestValueEncoder_Encode__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 TestValueEncoder_Encode__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 TestValueEncoder_Encode__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 TestValueEncoder_Encode__Rational(t *testing.T) {
|
||||
byteOrder := TestDefaultByteOrder
|
||||
ve := NewValueEncoder(byteOrder)
|
||||
|
||||
original := []Rational{
|
||||
{
|
||||
Numerator: 0x11,
|
||||
Denominator: 0x22,
|
||||
},
|
||||
{
|
||||
Numerator: 0x33,
|
||||
Denominator: 0x44,
|
||||
},
|
||||
{
|
||||
Numerator: 0x55,
|
||||
Denominator: 0x66,
|
||||
},
|
||||
{
|
||||
Numerator: 0x77,
|
||||
Denominator: 0x88,
|
||||
},
|
||||
{
|
||||
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 TestValueEncoder_Encode__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 TestValueEncoder_Encode__SignedRational(t *testing.T) {
|
||||
byteOrder := TestDefaultByteOrder
|
||||
ve := NewValueEncoder(byteOrder)
|
||||
|
||||
original := []SignedRational{
|
||||
{
|
||||
Numerator: 0x11,
|
||||
Denominator: 0x22,
|
||||
},
|
||||
{
|
||||
Numerator: 0x33,
|
||||
Denominator: 0x44,
|
||||
},
|
||||
{
|
||||
Numerator: 0x55,
|
||||
Denominator: 0x66,
|
||||
},
|
||||
{
|
||||
Numerator: 0x77,
|
||||
Denominator: 0x88,
|
||||
},
|
||||
{
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEncoder_Encode__Timestamp(t *testing.T) {
|
||||
byteOrder := TestDefaultByteOrder
|
||||
ve := NewValueEncoder(byteOrder)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
ed, err := ve.Encode(now)
|
||||
log.PanicIf(err)
|
||||
|
||||
if ed.Type != TypeAscii {
|
||||
t.Fatalf("Timestamp not encoded as ASCII.")
|
||||
}
|
||||
|
||||
expectedTimestampBytes := ExifFullTimestampString(now)
|
||||
|
||||
// Leave an extra byte for the NUL.
|
||||
expected := make([]byte, len(expectedTimestampBytes)+1)
|
||||
copy(expected, expectedTimestampBytes)
|
||||
|
||||
if bytes.Equal(ed.Encoded, expected) != true {
|
||||
t.Fatalf("Timestamp not encoded correctly: [%s] != [%s]", string(ed.Encoded), string(expected))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrTagNotFound indicates that the tag was not found.
|
||||
ErrTagNotFound = errors.New("tag not found")
|
||||
|
||||
// ErrTagNotKnown indicates that the tag is not registered with us as a
|
||||
// known tag.
|
||||
ErrTagNotKnown = errors.New("tag is not known")
|
||||
)
|
|
@ -0,0 +1,163 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2"
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
const (
|
||||
thumbnailFilenameIndexPlaceholder = "<index>"
|
||||
)
|
||||
|
||||
var (
|
||||
mainLogger = log.NewLogger("main.main")
|
||||
)
|
||||
|
||||
// IfdEntry is a JSON model for representing a single tag.
|
||||
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 exifcommon.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"`
|
||||
}
|
||||
|
||||
type parameters struct {
|
||||
Filepath string `short:"f" long:"filepath" required:"true" description:"File-path of image"`
|
||||
PrintAsJson bool `short:"j" long:"json" description:"Print out as JSON"`
|
||||
IsVerbose bool `short:"v" long:"verbose" description:"Print logging"`
|
||||
ThumbnailOutputFilepath string `short:"t" long:"thumbnail-output-filepath" description:"File-path to write thumbnail to (if present)"`
|
||||
DoNotPrintTags bool `short:"n" long:"no-tags" description:"Do not actually print tags. Good for auditing the logs or merely checking the EXIF structure for errors."`
|
||||
}
|
||||
|
||||
var (
|
||||
arguments = new(parameters)
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if errRaw := recover(); errRaw != nil {
|
||||
err := errRaw.(error)
|
||||
log.PrintError(err)
|
||||
|
||||
os.Exit(-2)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err := flags.Parse(arguments)
|
||||
if err != nil {
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
if arguments.IsVerbose == true {
|
||||
cla := log.NewConsoleLogAdapter()
|
||||
log.AddAdapter("console", cla)
|
||||
|
||||
scp := log.NewStaticConfigurationProvider()
|
||||
scp.SetLevelName(log.LevelNameDebug)
|
||||
|
||||
log.LoadConfiguration(scp)
|
||||
}
|
||||
|
||||
f, err := os.Open(arguments.Filepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
log.PanicIf(err)
|
||||
|
||||
rawExif, err := exif.SearchAndExtractExif(data)
|
||||
if err != nil {
|
||||
if err == exif.ErrNoExif {
|
||||
fmt.Printf("No EXIF data.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
mainLogger.Debugf(nil, "EXIF blob is (%d) bytes.", len(rawExif))
|
||||
|
||||
// Run the parse.
|
||||
|
||||
entries, err := exif.GetFlatExifData(rawExif)
|
||||
log.PanicIf(err)
|
||||
|
||||
// Write the thumbnail is requested and present.
|
||||
|
||||
thumbnailOutputFilepath := arguments.ThumbnailOutputFilepath
|
||||
if thumbnailOutputFilepath != "" {
|
||||
im := exif.NewIfdMappingWithStandard()
|
||||
ti := exif.NewTagIndex()
|
||||
|
||||
_, index, err := exif.Collect(im, ti, rawExif)
|
||||
log.PanicIf(err)
|
||||
|
||||
var thumbnail []byte
|
||||
if ifd, found := index.Lookup[exif.ThumbnailFqIfdPath]; found == true {
|
||||
thumbnail, err = ifd.Thumbnail()
|
||||
if err != nil && err != exif.ErrNoThumbnail {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if thumbnail == nil {
|
||||
mainLogger.Debugf(nil, "No thumbnails found.")
|
||||
} else {
|
||||
if arguments.PrintAsJson == false {
|
||||
fmt.Printf("Writing (%d) bytes for thumbnail: [%s]\n", len(thumbnail), thumbnailOutputFilepath)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
err := ioutil.WriteFile(thumbnailOutputFilepath, thumbnail, 0644)
|
||||
log.PanicIf(err)
|
||||
}
|
||||
}
|
||||
|
||||
if arguments.DoNotPrintTags == false {
|
||||
if arguments.PrintAsJson == true {
|
||||
data, err := json.MarshalIndent(entries, "", " ")
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Println(string(data))
|
||||
} else {
|
||||
thumbnailTags := 0
|
||||
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.Formatted)
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
|
||||
if thumbnailTags == 2 {
|
||||
fmt.Printf("There is a thumbnail.\n")
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
appFilepath := getAppFilepath()
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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=[Exif9101ComponentsConfiguration<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 35/1 0/1 0/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=[IFD1] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[[6]]
|
||||
IFD-PATH=[IFD1] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[72/1]]
|
||||
IFD-PATH=[IFD1] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[[72/1]]
|
||||
IFD-PATH=[IFD1] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[[2]]
|
||||
IFD-PATH=[IFD1] ID=(0x0201) NAME=[JPEGInterchangeFormat] COUNT=(1) TYPE=[LONG] VALUE=[[11444]]
|
||||
IFD-PATH=[IFD1] ID=(0x0202) NAME=[JPEGInterchangeFormatLength] COUNT=(1) TYPE=[LONG] VALUE=[[21491]]
|
||||
|
||||
`
|
||||
|
||||
if actual != expected {
|
||||
t.Fatalf("Output not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainJson(t *testing.T) {
|
||||
appFilepath := getAppFilepath()
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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.
|
||||
|
||||
assetsPath := exifcommon.GetTestAssetsPath()
|
||||
jsonFilepath := path.Join(assetsPath, "main_test_exif.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)
|
||||
// }
|
||||
|
||||
for i, tagInfo := range actual {
|
||||
if reflect.DeepEqual(tagInfo, expected[i]) == false {
|
||||
actualBytes, err := json.MarshalIndent(tagInfo, "", " ")
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedBytes, err := json.MarshalIndent(expected[i], "", " ")
|
||||
log.PanicIf(err)
|
||||
|
||||
t.Fatalf("Tag (%d) not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", i, string(actualBytes), string(expectedBytes))
|
||||
}
|
||||
}
|
||||
|
||||
if len(actual) != len(expected) {
|
||||
t.Fatalf("Actual tags not same length as expected tags.")
|
||||
}
|
||||
}
|
||||
|
||||
func getAppFilepath() string {
|
||||
moduleRootPath := exifcommon.GetModuleRootPath()
|
||||
appFilepath := path.Join(moduleRootPath, "exif-read-tool", "main.go")
|
||||
|
||||
return appFilepath
|
||||
}
|
||||
|
||||
func getTestImageFilepath() string {
|
||||
assetsPath := exifcommon.GetTestAssetsPath()
|
||||
testImageFilepath := path.Join(assetsPath, "NDM_8901.jpg")
|
||||
|
||||
return testImageFilepath
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
const (
|
||||
// ExifSignatureLength is the number of bytes in the EXIF signature (which
|
||||
// customarily includes the first IFD offset).
|
||||
ExifSignatureLength = 8
|
||||
)
|
||||
|
||||
var (
|
||||
exifLogger = log.NewLogger("exif.exif")
|
||||
|
||||
ExifBigEndianSignature = [4]byte{'M', 'M', 0x00, 0x2a}
|
||||
ExifLittleEndianSignature = [4]byte{'I', 'I', 0x2a, 0x00}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoExif = errors.New("no exif data")
|
||||
ErrExifHeaderError = errors.New("exif header error")
|
||||
)
|
||||
|
||||
// SearchAndExtractExif searches for an EXIF blob in the byte-slice.
|
||||
func SearchAndExtractExif(data []byte) (rawExif []byte, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
b := bytes.NewBuffer(data)
|
||||
|
||||
rawExif, err = SearchAndExtractExifWithReader(b)
|
||||
if err != nil {
|
||||
if err == ErrNoExif {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
return rawExif, nil
|
||||
}
|
||||
|
||||
// SearchAndExtractExifWithReader searches for an EXIF blob using an
|
||||
// `io.Reader`. We can't know how much long the EXIF data is without parsing it,
|
||||
// so this will likely grab up a lot of the image-data, too.
|
||||
func SearchAndExtractExifWithReader(r io.Reader) (rawExif []byte, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// Search for the beginning of the EXIF information. The EXIF is near the
|
||||
// beginning of most JPEGs, so this likely doesn't have a high cost (at
|
||||
// least, again, with JPEGs).
|
||||
|
||||
br := bufio.NewReader(r)
|
||||
discarded := 0
|
||||
|
||||
for {
|
||||
window, err := br.Peek(ExifSignatureLength)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, ErrNoExif
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
_, err = ParseExifHeader(window)
|
||||
if err != nil {
|
||||
if log.Is(err, ErrNoExif) == true {
|
||||
// No EXIF. Move forward by one byte.
|
||||
|
||||
_, err := br.Discard(1)
|
||||
log.PanicIf(err)
|
||||
|
||||
discarded++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Some other error.
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
exifLogger.Debugf(nil, "Found EXIF blob (%d) bytes from initial position.", discarded)
|
||||
|
||||
rawExif, err = ioutil.ReadAll(br)
|
||||
log.PanicIf(err)
|
||||
|
||||
return rawExif, 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))
|
||||
}
|
||||
}()
|
||||
|
||||
// Open the file.
|
||||
|
||||
f, err := os.Open(filepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
defer f.Close()
|
||||
|
||||
rawExif, err = SearchAndExtractExifWithReader(f)
|
||||
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) < ExifSignatureLength {
|
||||
exifLogger.Warningf(nil, "Not enough data for EXIF header: (%d)", len(data))
|
||||
return eh, ErrNoExif
|
||||
}
|
||||
|
||||
if bytes.Equal(data[:4], ExifBigEndianSignature[:]) == true {
|
||||
eh.ByteOrder = binary.BigEndian
|
||||
} else if bytes.Equal(data[:4], ExifLittleEndianSignature[:]) == true {
|
||||
eh.ByteOrder = binary.LittleEndian
|
||||
} else {
|
||||
return eh, ErrNoExif
|
||||
}
|
||||
|
||||
eh.FirstIfdOffset = eh.ByteOrder.Uint32(data[4:8])
|
||||
|
||||
return eh, nil
|
||||
}
|
||||
|
||||
// Visit recursively invokes a callback for every tag.
|
||||
func Visit(rootIfdIdentity *exifcommon.IfdIdentity, ifdMapping *exifcommon.IfdMapping, tagIndex *TagIndex, exifData []byte, visitor TagVisitorFn) (eh ExifHeader, furthestOffset uint32, 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(rootIfdIdentity, eh.FirstIfdOffset, visitor)
|
||||
log.PanicIf(err)
|
||||
|
||||
furthestOffset = ie.FurthestOffset()
|
||||
|
||||
return eh, furthestOffset, nil
|
||||
}
|
||||
|
||||
// Collect recursively builds a static structure of all IFDs and tags.
|
||||
func Collect(ifdMapping *exifcommon.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)
|
||||
log.PanicIf(err)
|
||||
|
||||
return eh, index, nil
|
||||
}
|
||||
|
||||
// BuildExifHeader constructs the bytes that go at the front of the stream.
|
||||
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)
|
||||
|
||||
var signatureBytes []byte
|
||||
if byteOrder == binary.BigEndian {
|
||||
signatureBytes = ExifBigEndianSignature[:]
|
||||
} else {
|
||||
signatureBytes = ExifLittleEndianSignature[:]
|
||||
}
|
||||
|
||||
_, err = b.Write(signatureBytes)
|
||||
log.PanicIf(err)
|
||||
|
||||
err = binary.Write(b, byteOrder, firstIfdOffset)
|
||||
log.PanicIf(err)
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
|
@ -0,0 +1,420 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestVisit(t *testing.T) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err := log.Wrap(state.(error))
|
||||
log.PrintError(err)
|
||||
|
||||
t.Fatalf("Test failure.")
|
||||
}
|
||||
}()
|
||||
|
||||
ti := NewTagIndex()
|
||||
|
||||
// Open the file.
|
||||
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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)
|
||||
|
||||
// DEPRECATED(dustin): fqIfdPath and ifdIndex are now redundant. Remove in next module version.
|
||||
visitor := func(fqIfdPath string, ifdIndex int, ite *IfdTagEntry) (err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
log.Panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
tagId := ite.TagId()
|
||||
tagType := ite.TagType()
|
||||
ii := ite.ifdIdentity
|
||||
|
||||
it, err := ti.Get(ii, tagId)
|
||||
if err != nil {
|
||||
if log.Is(err, ErrTagNotFound) {
|
||||
fmt.Printf("Unknown tag: [%s] (%04x)\n", ii.String(), tagId)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
valueString, err := ite.FormatFirst()
|
||||
log.PanicIf(err)
|
||||
|
||||
description := fmt.Sprintf("IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]", ii.String(), tagId, it.Name, ite.UnitCount(), tagType.String(), valueString)
|
||||
tags = append(tags, description)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
_, furthestOffset, err := Visit(exifcommon.IfdStandardIfdIdentity, im, ti, data[foundAt:], visitor)
|
||||
log.PanicIf(err)
|
||||
|
||||
if furthestOffset != 32935 {
|
||||
t.Fatalf("Furthest-offset is not valid: (%d)", furthestOffset)
|
||||
}
|
||||
|
||||
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=[Exif9101ComponentsConfiguration<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=[IFD1] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[6]",
|
||||
"IFD-PATH=[IFD1] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
|
||||
"IFD-PATH=[IFD1] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
|
||||
"IFD-PATH=[IFD1] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
|
||||
"IFD-PATH=[IFD1] ID=(0x0201) NAME=[JPEGInterchangeFormat] COUNT=(1) TYPE=[LONG] VALUE=[11444]",
|
||||
"IFD-PATH=[IFD1] ID=(0x0202) NAME=[JPEGInterchangeFormatLength] COUNT=(1) TYPE=[LONG] VALUE=[21491]",
|
||||
}
|
||||
|
||||
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) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
// Returns a slice starting with the EXIF data and going to the end of the
|
||||
// image.
|
||||
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
testExifData := getTestExifData()
|
||||
|
||||
if bytes.Compare(rawExif[:len(testExifData)], testExifData) != 0 {
|
||||
t.Fatalf("found EXIF data not correct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchAndExtractExif(t *testing.T) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
imageData, err := ioutil.ReadFile(testImageFilepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
rawExif, err := SearchAndExtractExif(imageData)
|
||||
log.PanicIf(err)
|
||||
|
||||
testExifData := getTestExifData()
|
||||
|
||||
if bytes.Compare(rawExif[:len(testExifData)], testExifData) != 0 {
|
||||
t.Fatalf("found EXIF data not correct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchAndExtractExifWithReader(t *testing.T) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
f, err := os.Open(testImageFilepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
defer f.Close()
|
||||
|
||||
rawExif, err := SearchAndExtractExifWithReader(f)
|
||||
log.PanicIf(err)
|
||||
|
||||
testExifData := getTestExifData()
|
||||
|
||||
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.PrintError(err)
|
||||
|
||||
t.Fatalf("Test failure.")
|
||||
}
|
||||
}()
|
||||
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
actualIfdPaths := make([]string, len(lookup))
|
||||
i := 0
|
||||
for ifdPath := range lookup {
|
||||
actualIfdPaths[i] = ifdPath
|
||||
i++
|
||||
}
|
||||
|
||||
sort.Strings(actualIfdPaths)
|
||||
|
||||
expectedIfdPaths := []string{
|
||||
"IFD",
|
||||
"IFD/Exif",
|
||||
"IFD/Exif/Iop",
|
||||
"IFD/GPSInfo",
|
||||
"IFD1",
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(actualIfdPaths, expectedIfdPaths) != true {
|
||||
t.Fatalf("The IFD lookup is not the right size: %v", actualIfdPaths)
|
||||
}
|
||||
|
||||
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.ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Root IFD is not labeled correctly: [%s]", rootIfd.ifdIdentity.UnindexedString())
|
||||
} else if rootIfd.NextIfd.ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Root IFD sibling is not labeled correctly: [%s]", rootIfd.ifdIdentity.UnindexedString())
|
||||
} else if rootIfd.Children[0].ifdIdentity.UnindexedString() != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Root IFD child (0) is not labeled correctly: [%s]", rootIfd.Children[0].ifdIdentity.UnindexedString())
|
||||
} else if rootIfd.Children[1].ifdIdentity.UnindexedString() != exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Root IFD child (1) is not labeled correctly: [%s]", rootIfd.Children[1].ifdIdentity.UnindexedString())
|
||||
} else if rootIfd.Children[0].Children[0].ifdIdentity.UnindexedString() != exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Exif IFD child is not an IOP IFD: [%s]", rootIfd.Children[0].Children[0].ifdIdentity.UnindexedString())
|
||||
}
|
||||
|
||||
if lookup[exifcommon.IfdStandardIfdIdentity.UnindexedString()].ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Lookup for standard IFD not correct.")
|
||||
} else if lookup[exifcommon.IfdStandardIfdIdentity.UnindexedString()+"1"].ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Lookup for standard IFD not correct.")
|
||||
}
|
||||
|
||||
if lookup[exifcommon.IfdExifStandardIfdIdentity.UnindexedString()].ifdIdentity.UnindexedString() != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Lookup for EXIF IFD not correct.")
|
||||
}
|
||||
|
||||
if lookup[exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString()].ifdIdentity.UnindexedString() != exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Lookup for GPS IFD not correct.")
|
||||
}
|
||||
|
||||
if lookup[exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString()].ifdIdentity.UnindexedString() != exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("Lookup for IOP IFD not correct.")
|
||||
}
|
||||
|
||||
foundExif := 0
|
||||
foundGps := 0
|
||||
for _, ite := range lookup[exifcommon.IfdStandardIfdIdentity.UnindexedString()].Entries {
|
||||
if ite.ChildIfdPath() == exifcommon.IfdExifStandardIfdIdentity.UnindexedString() {
|
||||
foundExif++
|
||||
|
||||
if ite.TagId() != exifcommon.IfdExifStandardIfdIdentity.TagId() {
|
||||
t.Fatalf("EXIF IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId(), exifcommon.IfdExifStandardIfdIdentity.TagId())
|
||||
}
|
||||
}
|
||||
|
||||
if ite.ChildIfdPath() == exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString() {
|
||||
foundGps++
|
||||
|
||||
if ite.TagId() != exifcommon.IfdGpsInfoStandardIfdIdentity.TagId() {
|
||||
t.Fatalf("GPS IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId(), exifcommon.IfdGpsInfoStandardIfdIdentity.TagId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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[exifcommon.IfdExifStandardIfdIdentity.UnindexedString()].Entries {
|
||||
if ite.ChildIfdPath() == exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString() {
|
||||
foundIop++
|
||||
|
||||
if ite.TagId() != exifcommon.IfdExifIopStandardIfdIdentity.TagId() {
|
||||
t.Fatalf("IOP IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId(), exifcommon.IfdExifIopStandardIfdIdentity.TagId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if foundIop != 1 {
|
||||
t.Fatalf("Exactly one IOP IFD tag wasn't found: (%d)", foundIop)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExifHeader(t *testing.T) {
|
||||
testExifData := getTestExifData()
|
||||
|
||||
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(exifcommon.TestDefaultByteOrder, 0x11223344)
|
||||
log.PanicIf(err)
|
||||
|
||||
eh, err := ParseExifHeader(headerBytes)
|
||||
log.PanicIf(err)
|
||||
|
||||
if eh.ByteOrder != exifcommon.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(exifcommon.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,14 @@
|
|||
module github.com/dsoprea/go-exif/v2
|
||||
|
||||
go 1.13
|
||||
|
||||
// Development only
|
||||
// replace github.com/dsoprea/go-logging => ../../go-logging
|
||||
|
||||
require (
|
||||
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
)
|
|
@ -0,0 +1,33 @@
|
|||
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/dsoprea/go-logging v0.0.0-20200502191043-ec333ec7635f h1:XM9MVftaUNA4CcjV97+4bSy7u9Ns04DEYbZkswUrRtc=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200502191043-ec333ec7635f/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200502201358-170ff607885f h1:FonKAuW3PmNtqk9tOR+Z7bnyQHytmnZBCmm5z1PQMss=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200502201358-170ff607885f/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d h1:F/7L5wr/fP/SKeO5HuMlNEX9Ipyx2MbH2rV9G4zJRpk=
|
||||
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
|
||||
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/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
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=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
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/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -0,0 +1,117 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
"github.com/golang/geo/s2"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrGpsCoordinatesNotValid means that some part of the geographic data was
|
||||
// unparseable.
|
||||
ErrGpsCoordinatesNotValid = errors.New("GPS coordinates not valid")
|
||||
)
|
||||
|
||||
// GpsDegrees is a high-level struct representing geographic data.
|
||||
type GpsDegrees struct {
|
||||
// Orientation describes the N/E/S/W direction that this position is
|
||||
// relative to.
|
||||
Orientation byte
|
||||
|
||||
// Degrees is a simple float representing the underlying rational degrees
|
||||
// amount.
|
||||
Degrees float64
|
||||
|
||||
// Minutes is a simple float representing the underlying rational minutes
|
||||
// amount.
|
||||
Minutes float64
|
||||
|
||||
// Seconds is a simple float representing the underlying ration seconds
|
||||
// amount.
|
||||
Seconds float64
|
||||
}
|
||||
|
||||
// NewGpsDegreesFromRationals returns a GpsDegrees struct given the EXIF-encoded
|
||||
// information. The refValue is the N/E/S/W direction that this position is
|
||||
// relative to.
|
||||
func NewGpsDegreesFromRationals(refValue string, rawCoordinate []exifcommon.Rational) (gd GpsDegrees, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
if len(rawCoordinate) != 3 {
|
||||
log.Panicf("new GpsDegrees struct requires a raw-coordinate with exactly three rationals")
|
||||
}
|
||||
|
||||
gd = GpsDegrees{
|
||||
Orientation: refValue[0],
|
||||
Degrees: float64(rawCoordinate[0].Numerator) / float64(rawCoordinate[0].Denominator),
|
||||
Minutes: float64(rawCoordinate[1].Numerator) / float64(rawCoordinate[1].Denominator),
|
||||
Seconds: float64(rawCoordinate[2].Numerator) / float64(rawCoordinate[2].Denominator),
|
||||
}
|
||||
|
||||
return gd, nil
|
||||
}
|
||||
|
||||
// String provides returns a descriptive string.
|
||||
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)
|
||||
}
|
||||
|
||||
// Decimal calculates and returns the simplified float representation of the
|
||||
// component degrees.
|
||||
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
|
||||
}
|
||||
|
||||
return decimal
|
||||
}
|
||||
|
||||
// Raw returns a Rational struct that can be used to *write* coordinates. In
|
||||
// practice, the denominator are typically (1) in the original EXIF data, and,
|
||||
// that being the case, this will best preserve precision.
|
||||
func (d GpsDegrees) Raw() []exifcommon.Rational {
|
||||
return []exifcommon.Rational{
|
||||
{Numerator: uint32(d.Degrees), Denominator: 1},
|
||||
{Numerator: uint32(d.Minutes), Denominator: 1},
|
||||
{Numerator: uint32(d.Seconds), Denominator: 1},
|
||||
}
|
||||
}
|
||||
|
||||
// GpsInfo encapsulates all of the geographic information in one place.
|
||||
type GpsInfo struct {
|
||||
Latitude, Longitude GpsDegrees
|
||||
Altitude int
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// String returns a descriptive string.
|
||||
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)
|
||||
}
|
||||
|
||||
// S2CellId returns the cell-ID of the geographic location on the earth.
|
||||
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,61 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestNewGpsDegreesFromRationals(t *testing.T) {
|
||||
latitudeRaw := []exifcommon.Rational{
|
||||
{Numerator: 22, Denominator: 2},
|
||||
{Numerator: 66, Denominator: 3},
|
||||
{Numerator: 132, Denominator: 4},
|
||||
}
|
||||
|
||||
gd, err := NewGpsDegreesFromRationals("W", latitudeRaw)
|
||||
log.PanicIf(err)
|
||||
|
||||
if gd.Orientation != 'W' {
|
||||
t.Fatalf("Orientation was not set correctly: [%s]", string([]byte{gd.Orientation}))
|
||||
}
|
||||
|
||||
degreesRightBound := math.Nextafter(11.0, 12.0)
|
||||
minutesRightBound := math.Nextafter(22.0, 23.0)
|
||||
secondsRightBound := math.Nextafter(33.0, 34.0)
|
||||
|
||||
if gd.Degrees < 11.0 || gd.Degrees >= degreesRightBound {
|
||||
t.Fatalf("Degrees is not correct: (%.2f)", gd.Degrees)
|
||||
} else if gd.Minutes < 22.0 || gd.Minutes >= minutesRightBound {
|
||||
t.Fatalf("Minutes is not correct: (%.2f)", gd.Minutes)
|
||||
} else if gd.Seconds < 33.0 || gd.Seconds >= secondsRightBound {
|
||||
t.Fatalf("Seconds is not correct: (%.2f)", gd.Seconds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGpsDegrees_Raw(t *testing.T) {
|
||||
latitudeRaw := []exifcommon.Rational{
|
||||
{Numerator: 22, Denominator: 2},
|
||||
{Numerator: 66, Denominator: 3},
|
||||
{Numerator: 132, Denominator: 4},
|
||||
}
|
||||
|
||||
gd, err := NewGpsDegreesFromRationals("W", latitudeRaw)
|
||||
log.PanicIf(err)
|
||||
|
||||
actual := gd.Raw()
|
||||
|
||||
expected := []exifcommon.Rational{
|
||||
{Numerator: 11, Denominator: 1},
|
||||
{Numerator: 22, Denominator: 1},
|
||||
{Numerator: 33, Denominator: 1},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(actual, expected) != true {
|
||||
t.Fatalf("GpsInfo not correctly encoded down to raw: %v\n", actual)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
// TODO(dustin): This file now exists for backwards-compatibility only.
|
||||
|
||||
// NewIfdMapping returns a new IfdMapping struct.
|
||||
//
|
||||
// RELEASE(dustin): This is a bridging function for backwards-compatibility. Remove this in the next release.
|
||||
func NewIfdMapping() (ifdMapping *exifcommon.IfdMapping) {
|
||||
return exifcommon.NewIfdMapping()
|
||||
}
|
||||
|
||||
// NewIfdMappingWithStandard retruns a new IfdMapping struct preloaded with the
|
||||
// standard IFDs.
|
||||
//
|
||||
// RELEASE(dustin): This is a bridging function for backwards-compatibility. Remove this in the next release.
|
||||
func NewIfdMappingWithStandard() (ifdMapping *exifcommon.IfdMapping) {
|
||||
return exifcommon.NewIfdMappingWithStandard()
|
||||
}
|
||||
|
||||
// LoadStandardIfds loads the standard IFDs into the mapping.
|
||||
//
|
||||
// RELEASE(dustin): This is a bridging function for backwards-compatibility. Remove this in the next release.
|
||||
func LoadStandardIfds(im *exifcommon.IfdMapping) (err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
err = exifcommon.LoadStandardIfds(im)
|
||||
log.PanicIf(err)
|
||||
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,532 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
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 == exifcommon.TypeUndefined {
|
||||
effectiveType = exifcommon.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.IfdIdentity().UnindexedString(), bt.value.Ib().IfdIdentity().UnindexedString())
|
||||
|
||||
// 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().IfdIdentity().UnindexedString(), ib.IfdIdentity().UnindexedString())
|
||||
|
||||
// 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().IfdIdentity().UnindexedString())
|
||||
|
||||
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.IfdIdentity().UnindexedString())
|
||||
|
||||
ibe.pushToJournal("encodeAndAttachIfd", ">", "Calculating size: (%d) [%s]", i, thisIb.IfdIdentity().UnindexedString())
|
||||
|
||||
_, tableSize, allocatedDataSize, _, err := ibe.encodeIfdToBytes(thisIb, ifdAddressableOffset, 0, false)
|
||||
log.PanicIf(err)
|
||||
|
||||
ibe.pushToJournal("encodeAndAttachIfd", "<", "Finished calculating size: (%d) [%s]", i, thisIb.IfdIdentity().UnindexedString())
|
||||
|
||||
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.IfdIdentity().UnindexedString(), 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.IfdIdentity().UnindexedString())
|
||||
|
||||
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.IfdIdentity().UnindexedString(), 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,894 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func Test_ByteWriter_writeAsBytes_uint8(t *testing.T) {
|
||||
b := new(bytes.Buffer)
|
||||
bw := NewByteWriter(b, exifcommon.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, exifcommon.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, exifcommon.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, exifcommon.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, exifcommon.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, exifcommon.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, exifcommon.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, exifcommon.IfdGpsInfoStandardIfdIdentity, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
it, err := ti.Get(ib.IfdIdentity(), uint16(0x0000))
|
||||
log.PanicIf(err)
|
||||
|
||||
bt := NewStandardBuilderTag(exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), it, exifcommon.TestDefaultByteOrder, []uint8{uint8(0x12)})
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
bw := NewByteWriter(b, exifcommon.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, exifcommon.IfdGpsInfoStandardIfdIdentity, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
it, err := ti.Get(ib.IfdIdentity(), uint16(0x0000))
|
||||
log.PanicIf(err)
|
||||
|
||||
bt := NewStandardBuilderTag(exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), it, exifcommon.TestDefaultByteOrder, []uint8{uint8(0x12), uint8(0x34), uint8(0x56), uint8(0x78)})
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
bw := NewByteWriter(b, exifcommon.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, exifcommon.IfdGpsInfoStandardIfdIdentity, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
bw := NewByteWriter(b, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
addressableOffset := uint32(0x1234)
|
||||
ida := newIfdDataAllocator(addressableOffset)
|
||||
|
||||
it, err := ti.Get(ib.IfdIdentity(), uint16(0x0000))
|
||||
log.PanicIf(err)
|
||||
|
||||
bt := NewStandardBuilderTag(exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), it, exifcommon.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(exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(), it, exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
bw := NewByteWriter(b, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
addressableOffset := uint32(0x1234)
|
||||
ida := newIfdDataAllocator(addressableOffset)
|
||||
|
||||
childIb := NewIfdBuilder(im, ti, exifcommon.IfdExifStandardIfdIdentity, exifcommon.TestDefaultByteOrder)
|
||||
tagValue := NewIfdBuilderTagValueFromIfdBuilder(childIb)
|
||||
bt := NewChildIfdBuilderTag(exifcommon.IfdStandardIfdIdentity.UnindexedString(), exifcommon.IfdExifStandardIfdIdentity.TagId(), 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, exifcommon.IfdExifStandardIfdIdentity, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
childIbTestTag := &BuilderTag{
|
||||
ifdPath: exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
tagId: 0x8822,
|
||||
typeId: exifcommon.TypeShort,
|
||||
value: NewIfdBuilderTagValueFromBytes([]byte{0x12, 0x34}),
|
||||
}
|
||||
|
||||
childIb.Add(childIbTestTag)
|
||||
|
||||
// Formally compose the tag that refers to it.
|
||||
|
||||
tagValue := NewIfdBuilderTagValueFromIfdBuilder(childIb)
|
||||
bt := NewChildIfdBuilderTag(exifcommon.IfdStandardIfdIdentity.UnindexedString(), exifcommon.IfdExifStandardIfdIdentity.TagId(), 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, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
bw := NewByteWriter(b, exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder, tagBytes)
|
||||
log.PanicIf(err)
|
||||
|
||||
if iteV.TagId() != exifcommon.IfdExifStandardIfdIdentity.TagId() {
|
||||
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() != exifcommon.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.getValueOffset() != nextIfdOffsetToWrite {
|
||||
t.Fatalf("IFD's child-IFD offset (as offset) is not correct: (%d) != (%d)", iteV.getValueOffset(), nextIfdOffsetToWrite)
|
||||
} else if iteV.ChildIfdPath() != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("IFD first tag IFD-name name not correct: [%s]", iteV.ChildIfdPath())
|
||||
} else if iteV.IfdPath() != exifcommon.IfdStandardIfdIdentity.UnindexedString() {
|
||||
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, exifcommon.IfdExifStandardIfdIdentity, exifcommon.TestDefaultByteOrder, childIfdBlock, nil)
|
||||
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() != exifcommon.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.ChildIfdPath() != "" {
|
||||
t.Fatalf("Child IFD first tag IFD-name name not empty: [%s]", ite.ChildIfdPath())
|
||||
} else if ite.IfdPath() != exifcommon.IfdExifStandardIfdIdentity.UnindexedString() {
|
||||
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, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
it, err := ib.tagIndex.Get(ib.IfdIdentity(), uint16(0x000b))
|
||||
log.PanicIf(err)
|
||||
|
||||
valueString := "testvalue"
|
||||
bt := NewStandardBuilderTag(exifcommon.IfdStandardIfdIdentity.UnindexedString(), it, exifcommon.TestDefaultByteOrder, valueString)
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
bw := NewByteWriter(b, exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.TestDefaultByteOrder, tagBytes)
|
||||
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() != exifcommon.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.ChildIfdPath() != "" {
|
||||
t.Fatalf("Tag's IFD-name should be empty: [%s]", ite.ChildIfdPath())
|
||||
} else if ite.IfdPath() != exifcommon.IfdStandardIfdIdentity.UnindexedString() {
|
||||
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", exifcommon.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(exifcommon.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(exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.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, exifcommon.IfdExifStandardIfdIdentity, exifcommon.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, exifcommon.IfdGpsInfoStandardIfdIdentity, exifcommon.TestDefaultByteOrder)
|
||||
|
||||
err = childIb2.AddStandardWithName("GPSAltitudeRef", []uint8{0x11, 0x22})
|
||||
log.PanicIf(err)
|
||||
|
||||
err = ib.AddChildIb(childIb2)
|
||||
log.PanicIf(err)
|
||||
|
||||
err = ib.AddStandard(0x013e, []exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.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", []exifcommon.Rational{{Numerator: 0x11112222, Denominator: 0x33334444}})
|
||||
log.PanicIf(err)
|
||||
|
||||
err = ib.AddStandardWithName("ShutterSpeedValue", []exifcommon.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)
|
||||
|
||||
for i, ite := range index.RootIfd.Entries {
|
||||
value, err := ite.Value()
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Printf("%d: %s [%v]\n", i, ite, 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,585 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestIfdTagEntry_RawBytes_RealData(t *testing.T) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err := log.Wrap(state.(error))
|
||||
log.PrintErrorf(err, "Test failure.")
|
||||
}
|
||||
}()
|
||||
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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)
|
||||
|
||||
var ite *IfdTagEntry
|
||||
for _, thisIte := range index.RootIfd.Entries {
|
||||
if thisIte.TagId() == 0x0110 {
|
||||
ite = thisIte
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ite == nil {
|
||||
t.Fatalf("Tag not found.")
|
||||
}
|
||||
|
||||
decodedBytes, err := ite.GetRawBytes()
|
||||
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) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
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)
|
||||
log.PanicIf(err)
|
||||
|
||||
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) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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")
|
||||
log.PanicIf(err)
|
||||
|
||||
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) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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, ErrTagNotKnown) == false {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfd_Thumbnail(t *testing.T) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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)
|
||||
|
||||
assetsPath := exifcommon.GetTestAssetsPath()
|
||||
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 := getTestGpsImageFilepath()
|
||||
|
||||
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(exifcommon.IfdGpsInfoStandardIfdIdentity)
|
||||
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_GpsInfo__2_0_0_0(t *testing.T) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err := log.Wrap(state.(error))
|
||||
log.PrintErrorf(err, "Test failure.")
|
||||
}
|
||||
}()
|
||||
|
||||
assetsPath := exifcommon.GetTestAssetsPath()
|
||||
filepath := path.Join(assetsPath, "gps-2000-scaled.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(exifcommon.IfdGpsInfoStandardIfdIdentity)
|
||||
log.PanicIf(err)
|
||||
|
||||
gi, err := ifd.GpsInfo()
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedLatitude := GpsDegrees{
|
||||
Orientation: 'S',
|
||||
Degrees: 38.0,
|
||||
Minutes: 24.311687,
|
||||
Seconds: 0.0,
|
||||
}
|
||||
|
||||
expectedLongitude := GpsDegrees{
|
||||
Orientation: 'E',
|
||||
Degrees: 144.0,
|
||||
Minutes: 11.33748,
|
||||
Seconds: 0.0,
|
||||
}
|
||||
|
||||
if GpsDegreesEquals(gi.Latitude, expectedLatitude) != true {
|
||||
t.Fatalf("Latitude not correct: %v", gi.Latitude)
|
||||
} else if GpsDegreesEquals(gi.Longitude, expectedLongitude) != true {
|
||||
t.Fatalf("Longitude not correct: %v", gi.Longitude)
|
||||
} else if gi.Altitude != 0 {
|
||||
t.Fatalf("Altitude not correct: (%d)", gi.Altitude)
|
||||
} else if gi.Timestamp.Unix() != -62135596800 {
|
||||
t.Fatalf("Timestamp not correct: (%d)", gi.Timestamp.Unix())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfd_EnumerateTagsRecursively(t *testing.T) {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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.ifdIdentity.UnindexedString(),
|
||||
int(ite.TagId()),
|
||||
}
|
||||
|
||||
collected = append(collected, item)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err = index.RootIfd.EnumerateTagsRecursively(cb)
|
||||
log.PanicIf(err)
|
||||
|
||||
expected := [][2]interface{}{
|
||||
{"IFD", 0x010f},
|
||||
{"IFD", 0x0110},
|
||||
{"IFD", 0x0112},
|
||||
{"IFD", 0x011a},
|
||||
{"IFD", 0x011b},
|
||||
{"IFD", 0x0128},
|
||||
{"IFD", 0x0132},
|
||||
{"IFD", 0x013b},
|
||||
{"IFD", 0x0213},
|
||||
{"IFD", 0x8298},
|
||||
{"IFD/Exif", 0x829a},
|
||||
{"IFD/Exif", 0x829d},
|
||||
{"IFD/Exif", 0x8822},
|
||||
{"IFD/Exif", 0x8827},
|
||||
{"IFD/Exif", 0x8830},
|
||||
{"IFD/Exif", 0x8832},
|
||||
{"IFD/Exif", 0x9000},
|
||||
{"IFD/Exif", 0x9003},
|
||||
{"IFD/Exif", 0x9004},
|
||||
{"IFD/Exif", 0x9101},
|
||||
{"IFD/Exif", 0x9201},
|
||||
{"IFD/Exif", 0x9202},
|
||||
{"IFD/Exif", 0x9204},
|
||||
{"IFD/Exif", 0x9207},
|
||||
{"IFD/Exif", 0x9209},
|
||||
{"IFD/Exif", 0x920a},
|
||||
{"IFD/Exif", 0x927c},
|
||||
{"IFD/Exif", 0x9286},
|
||||
{"IFD/Exif", 0x9290},
|
||||
{"IFD/Exif", 0x9291},
|
||||
{"IFD/Exif", 0x9292},
|
||||
{"IFD/Exif", 0xa000},
|
||||
{"IFD/Exif", 0xa001},
|
||||
{"IFD/Exif", 0xa002},
|
||||
{"IFD/Exif", 0xa003},
|
||||
{"IFD/Exif/Iop", 0x0001},
|
||||
{"IFD/Exif/Iop", 0x0002},
|
||||
{"IFD/Exif", 0xa20e},
|
||||
{"IFD/Exif", 0xa20f},
|
||||
{"IFD/Exif", 0xa210},
|
||||
{"IFD/Exif", 0xa401},
|
||||
{"IFD/Exif", 0xa402},
|
||||
{"IFD/Exif", 0xa403},
|
||||
{"IFD/Exif", 0xa406},
|
||||
{"IFD/Exif", 0xa430},
|
||||
{"IFD/Exif", 0xa431},
|
||||
{"IFD/Exif", 0xa432},
|
||||
{"IFD/Exif", 0xa434},
|
||||
{"IFD/Exif", 0xa435},
|
||||
{"IFD/GPSInfo", 0x0000},
|
||||
{"IFD", 0x010f},
|
||||
{"IFD", 0x0110},
|
||||
{"IFD", 0x0112},
|
||||
{"IFD", 0x011a},
|
||||
{"IFD", 0x011b},
|
||||
{"IFD", 0x0128},
|
||||
{"IFD", 0x0132},
|
||||
{"IFD", 0x013b},
|
||||
{"IFD", 0x0213},
|
||||
{"IFD", 0x8298},
|
||||
{"IFD/Exif", 0x829a},
|
||||
{"IFD/Exif", 0x829d},
|
||||
{"IFD/Exif", 0x8822},
|
||||
{"IFD/Exif", 0x8827},
|
||||
{"IFD/Exif", 0x8830},
|
||||
{"IFD/Exif", 0x8832},
|
||||
{"IFD/Exif", 0x9000},
|
||||
{"IFD/Exif", 0x9003},
|
||||
{"IFD/Exif", 0x9004},
|
||||
{"IFD/Exif", 0x9101},
|
||||
{"IFD/Exif", 0x9201},
|
||||
{"IFD/Exif", 0x9202},
|
||||
{"IFD/Exif", 0x9204},
|
||||
{"IFD/Exif", 0x9207},
|
||||
{"IFD/Exif", 0x9209},
|
||||
{"IFD/Exif", 0x920a},
|
||||
{"IFD/Exif", 0x927c},
|
||||
{"IFD/Exif", 0x9286},
|
||||
{"IFD/Exif", 0x9290},
|
||||
{"IFD/Exif", 0x9291},
|
||||
{"IFD/Exif", 0x9292},
|
||||
{"IFD/Exif", 0xa000},
|
||||
{"IFD/Exif", 0xa001},
|
||||
{"IFD/Exif", 0xa002},
|
||||
{"IFD/Exif", 0xa003},
|
||||
{"IFD/Exif/Iop", 0x0001},
|
||||
{"IFD/Exif/Iop", 0x0002},
|
||||
{"IFD/Exif", 0xa20e},
|
||||
{"IFD/Exif", 0xa20f},
|
||||
{"IFD/Exif", 0xa210},
|
||||
{"IFD/Exif", 0xa401},
|
||||
{"IFD/Exif", 0xa402},
|
||||
{"IFD/Exif", 0xa403},
|
||||
{"IFD/Exif", 0xa406},
|
||||
{"IFD/Exif", 0xa430},
|
||||
{"IFD/Exif", 0xa431},
|
||||
{"IFD/Exif", 0xa432},
|
||||
{"IFD/Exif", 0xa434},
|
||||
{"IFD/Exif", 0xa435},
|
||||
{"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() {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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 := getTestGpsImageFilepath()
|
||||
|
||||
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(exifcommon.IfdGpsInfoStandardIfdIdentity)
|
||||
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() {
|
||||
testImageFilepath := getTestImageFilepath()
|
||||
|
||||
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"
|
||||
|
||||
rootIfd := index.RootIfd
|
||||
|
||||
// We know the tag we want is on IFD0 (the first/root IFD).
|
||||
results, err := 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 := ite.Value()
|
||||
log.PanicIf(err)
|
||||
|
||||
value := valueRaw.(string)
|
||||
fmt.Println(value)
|
||||
|
||||
// Output:
|
||||
// Canon EOS 5D Mark III
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
"github.com/dsoprea/go-exif/v2/undefined"
|
||||
)
|
||||
|
||||
var (
|
||||
iteLogger = log.NewLogger("exif.ifd_tag_entry")
|
||||
)
|
||||
|
||||
// IfdTagEntry refers to a tag in the loaded EXIF block.
|
||||
type IfdTagEntry struct {
|
||||
tagId uint16
|
||||
tagIndex int
|
||||
tagType exifcommon.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 accommodate it for a consistent experience.
|
||||
|
||||
ifdIdentity *exifcommon.IfdIdentity
|
||||
|
||||
isUnhandledUnknown bool
|
||||
|
||||
addressableData []byte
|
||||
byteOrder binary.ByteOrder
|
||||
|
||||
tagName string
|
||||
}
|
||||
|
||||
func newIfdTagEntry(ii *exifcommon.IfdIdentity, tagId uint16, tagIndex int, tagType exifcommon.TagTypePrimitive, unitCount uint32, valueOffset uint32, rawValueOffset []byte, addressableData []byte, byteOrder binary.ByteOrder) *IfdTagEntry {
|
||||
return &IfdTagEntry{
|
||||
ifdIdentity: ii,
|
||||
tagId: tagId,
|
||||
tagIndex: tagIndex,
|
||||
tagType: tagType,
|
||||
unitCount: unitCount,
|
||||
valueOffset: valueOffset,
|
||||
rawValueOffset: rawValueOffset,
|
||||
addressableData: addressableData,
|
||||
byteOrder: byteOrder,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a stringified representation of the struct.
|
||||
func (ite *IfdTagEntry) String() string {
|
||||
return fmt.Sprintf("IfdTagEntry<TAG-IFD-PATH=[%s] TAG-ID=(0x%04x) TAG-TYPE=[%s] UNIT-COUNT=(%d)>", ite.ifdIdentity.String(), ite.tagId, ite.tagType.String(), ite.unitCount)
|
||||
}
|
||||
|
||||
// TagName returns the name of the tag. This is determined else and set after
|
||||
// the parse (since it's not actually stored in the stream). If it's empty, it
|
||||
// is because it is an unknown tag (nonstandard or otherwise unavailable in the
|
||||
// tag-index).
|
||||
func (ite *IfdTagEntry) TagName() string {
|
||||
return ite.tagName
|
||||
}
|
||||
|
||||
// setTagName sets the tag-name. This provides the name for convenience and
|
||||
// efficiency by determining it when most efficient while we're parsing rather
|
||||
// than delegating it to the caller (or, worse, the user).
|
||||
func (ite *IfdTagEntry) setTagName(tagName string) {
|
||||
ite.tagName = tagName
|
||||
}
|
||||
|
||||
// IfdPath returns the fully-qualified path of the IFD that owns this tag.
|
||||
func (ite *IfdTagEntry) IfdPath() string {
|
||||
return ite.ifdIdentity.String()
|
||||
}
|
||||
|
||||
// TagId returns the ID of the tag that we represent. The combination of
|
||||
// (IfdPath(), TagId()) is unique.
|
||||
func (ite *IfdTagEntry) TagId() uint16 {
|
||||
return ite.tagId
|
||||
}
|
||||
|
||||
// IsThumbnailOffset returns true if the tag has the IFD and tag-ID of a
|
||||
// thumbnail offset.
|
||||
func (ite *IfdTagEntry) IsThumbnailOffset() bool {
|
||||
return ite.tagId == ThumbnailOffsetTagId && ite.ifdIdentity.String() == ThumbnailFqIfdPath
|
||||
}
|
||||
|
||||
// IsThumbnailSize returns true if the tag has the IFD and tag-ID of a thumbnail
|
||||
// size.
|
||||
func (ite *IfdTagEntry) IsThumbnailSize() bool {
|
||||
return ite.tagId == ThumbnailSizeTagId && ite.ifdIdentity.String() == ThumbnailFqIfdPath
|
||||
}
|
||||
|
||||
// TagType is the type of value for this tag.
|
||||
func (ite *IfdTagEntry) TagType() exifcommon.TagTypePrimitive {
|
||||
return ite.tagType
|
||||
}
|
||||
|
||||
// updateTagType sets an alternatively interpreted tag-type.
|
||||
func (ite *IfdTagEntry) updateTagType(tagType exifcommon.TagTypePrimitive) {
|
||||
ite.tagType = tagType
|
||||
}
|
||||
|
||||
// UnitCount returns the unit-count of the tag's value.
|
||||
func (ite *IfdTagEntry) UnitCount() uint32 {
|
||||
return ite.unitCount
|
||||
}
|
||||
|
||||
// updateUnitCount sets an alternatively interpreted unit-count.
|
||||
func (ite *IfdTagEntry) updateUnitCount(unitCount uint32) {
|
||||
ite.unitCount = unitCount
|
||||
}
|
||||
|
||||
// getValueOffset is the four-byte offset converted to an integer to point to
|
||||
// the location of its value in the EXIF block. The "get" parameter is obviously
|
||||
// used in order to differentiate the naming of the method from the field.
|
||||
func (ite *IfdTagEntry) getValueOffset() uint32 {
|
||||
return ite.valueOffset
|
||||
}
|
||||
|
||||
// GetRawBytes renders a specific list of bytes from the value in this tag.
|
||||
func (ite *IfdTagEntry) GetRawBytes() (rawBytes []byte, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext := ite.getValueContext()
|
||||
|
||||
if ite.tagType == exifcommon.TypeUndefined {
|
||||
value, err := exifundefined.Decode(valueContext)
|
||||
if err != nil {
|
||||
if err == exifcommon.ErrUnhandledUndefinedTypedTag {
|
||||
ite.setIsUnhandledUnknown(true)
|
||||
return nil, exifundefined.ErrUnparseableValue
|
||||
} else if err == exifundefined.ErrUnparseableValue {
|
||||
return nil, err
|
||||
} else {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Encode it back, in order to get the raw bytes. This is the best,
|
||||
// general way to do it with an undefined tag.
|
||||
|
||||
rawBytes, _, err := exifundefined.Encode(value, ite.byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
return rawBytes, nil
|
||||
}
|
||||
|
||||
rawBytes, err = valueContext.ReadRawEncoded()
|
||||
log.PanicIf(err)
|
||||
|
||||
return rawBytes, nil
|
||||
}
|
||||
|
||||
// Value returns the specific, parsed, typed value from the tag.
|
||||
func (ite *IfdTagEntry) Value() (value interface{}, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext := ite.getValueContext()
|
||||
|
||||
if ite.tagType == exifcommon.TypeUndefined {
|
||||
var err error
|
||||
|
||||
value, err = exifundefined.Decode(valueContext)
|
||||
if err != nil {
|
||||
if err == exifcommon.ErrUnhandledUndefinedTypedTag || err == exifundefined.ErrUnparseableValue {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
|
||||
value, err = valueContext.Values()
|
||||
log.PanicIf(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Format returns the tag's value as a string.
|
||||
func (ite *IfdTagEntry) Format() (phrase string, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
value, err := ite.Value()
|
||||
if err != nil {
|
||||
if err == exifcommon.ErrUnhandledUndefinedTypedTag {
|
||||
return exifundefined.UnparseableUnknownTagValuePlaceholder, nil
|
||||
} else if err == exifundefined.ErrUnparseableValue {
|
||||
return exifundefined.UnparseableHandledTagValuePlaceholder, nil
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
phrase, err = exifcommon.FormatFromType(value, false)
|
||||
log.PanicIf(err)
|
||||
|
||||
return phrase, nil
|
||||
}
|
||||
|
||||
// FormatFirst returns the same as Format() but only the first item.
|
||||
func (ite *IfdTagEntry) FormatFirst() (phrase string, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): We should add a convenience type "timestamp", to simplify translating to and from the physical ASCII and provide validation.
|
||||
|
||||
value, err := ite.Value()
|
||||
if err != nil {
|
||||
if err == exifcommon.ErrUnhandledUndefinedTypedTag {
|
||||
return exifundefined.UnparseableUnknownTagValuePlaceholder, nil
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
phrase, err = exifcommon.FormatFromType(value, true)
|
||||
log.PanicIf(err)
|
||||
|
||||
return phrase, nil
|
||||
}
|
||||
|
||||
func (ite *IfdTagEntry) setIsUnhandledUnknown(isUnhandledUnknown bool) {
|
||||
ite.isUnhandledUnknown = isUnhandledUnknown
|
||||
}
|
||||
|
||||
// SetChildIfd sets child-IFD information (if we represent a child IFD).
|
||||
func (ite *IfdTagEntry) SetChildIfd(ii *exifcommon.IfdIdentity) {
|
||||
ite.childFqIfdPath = ii.String()
|
||||
ite.childIfdPath = ii.UnindexedString()
|
||||
ite.childIfdName = ii.Name()
|
||||
}
|
||||
|
||||
// ChildIfdName returns the name of the child IFD
|
||||
func (ite *IfdTagEntry) ChildIfdName() string {
|
||||
return ite.childIfdName
|
||||
}
|
||||
|
||||
// ChildIfdPath returns the path of the child IFD.
|
||||
func (ite *IfdTagEntry) ChildIfdPath() string {
|
||||
return ite.childIfdPath
|
||||
}
|
||||
|
||||
// ChildFqIfdPath returns the complete path of the child IFD along with the
|
||||
// numeric suffixes differentiating sibling occurrences of the same type. "0"
|
||||
// indices are omitted.
|
||||
func (ite *IfdTagEntry) ChildFqIfdPath() string {
|
||||
return ite.childFqIfdPath
|
||||
}
|
||||
|
||||
// IfdIdentity returns the IfdIdentity associated with this tag.
|
||||
func (ite *IfdTagEntry) IfdIdentity() *exifcommon.IfdIdentity {
|
||||
return ite.ifdIdentity
|
||||
}
|
||||
|
||||
func (ite *IfdTagEntry) getValueContext() *exifcommon.ValueContext {
|
||||
return exifcommon.NewValueContext(
|
||||
ite.ifdIdentity.String(),
|
||||
ite.tagId,
|
||||
ite.unitCount,
|
||||
ite.valueOffset,
|
||||
ite.rawValueOffset,
|
||||
ite.addressableData,
|
||||
ite.tagType,
|
||||
ite.byteOrder)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestIfdTagEntry_RawBytes_Allocated(t *testing.T) {
|
||||
data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
||||
|
||||
addressableBytes := data
|
||||
|
||||
ite := newIfdTagEntry(
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
0x1,
|
||||
0,
|
||||
exifcommon.TypeByte,
|
||||
6,
|
||||
0,
|
||||
nil,
|
||||
addressableBytes,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
value, err := ite.GetRawBytes()
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Compare(value, data) != 0 {
|
||||
t.Fatalf("Value not expected: [%s] != [%s]", value, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfdTagEntry_RawBytes_Embedded(t *testing.T) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err := log.Wrap(state.(error))
|
||||
log.PrintError(err)
|
||||
|
||||
t.Fatalf("Test failure.")
|
||||
}
|
||||
}()
|
||||
|
||||
data := []byte{0x11, 0x22, 0x33, 0x44}
|
||||
|
||||
ite := newIfdTagEntry(
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
0x1,
|
||||
0,
|
||||
exifcommon.TypeByte,
|
||||
4,
|
||||
0,
|
||||
data,
|
||||
nil,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
value, err := ite.GetRawBytes()
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Compare(value, data) != 0 {
|
||||
t.Fatalf("Value not expected: %v != %v", value, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfdTagEntry_String(t *testing.T) {
|
||||
ite := newIfdTagEntry(
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
0x1,
|
||||
0,
|
||||
exifcommon.TypeByte,
|
||||
6,
|
||||
0,
|
||||
nil,
|
||||
nil,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// Package 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,411 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
const (
|
||||
// IFD1
|
||||
|
||||
// ThumbnailFqIfdPath is the fully-qualified IFD path that the thumbnail
|
||||
// must be found in.
|
||||
ThumbnailFqIfdPath = "IFD1"
|
||||
|
||||
// ThumbnailOffsetTagId returns the tag-ID of the thumbnail offset.
|
||||
ThumbnailOffsetTagId = 0x0201
|
||||
|
||||
// ThumbnailSizeTagId returns the tag-ID of the thumbnail size.
|
||||
ThumbnailSizeTagId = 0x0202
|
||||
)
|
||||
|
||||
const (
|
||||
// GPS
|
||||
|
||||
// TagGpsVersionId is the ID of the GPS version tag.
|
||||
TagGpsVersionId = 0x0000
|
||||
|
||||
// TagLatitudeId is the ID of the GPS latitude tag.
|
||||
TagLatitudeId = 0x0002
|
||||
|
||||
// TagLatitudeRefId is the ID of the GPS latitude orientation tag.
|
||||
TagLatitudeRefId = 0x0001
|
||||
|
||||
// TagLongitudeId is the ID of the GPS longitude tag.
|
||||
TagLongitudeId = 0x0004
|
||||
|
||||
// TagLongitudeRefId is the ID of the GPS longitude-orientation tag.
|
||||
TagLongitudeRefId = 0x0003
|
||||
|
||||
// TagTimestampId is the ID of the GPS time tag.
|
||||
TagTimestampId = 0x0007
|
||||
|
||||
// TagDatestampId is the ID of the GPS date tag.
|
||||
TagDatestampId = 0x001d
|
||||
|
||||
// TagAltitudeId is the ID of the GPS altitude tag.
|
||||
TagAltitudeId = 0x0006
|
||||
|
||||
// TagAltitudeRefId is the ID of the GPS altitude-orientation tag.
|
||||
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: {},
|
||||
}
|
||||
)
|
||||
|
||||
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"`
|
||||
TypeNames []string `yaml:"type_names"`
|
||||
}
|
||||
|
||||
// Indexing structures.
|
||||
|
||||
// IndexedTag describes one index lookup result.
|
||||
type IndexedTag struct {
|
||||
// Id is the tag-ID.
|
||||
Id uint16
|
||||
|
||||
// Name is the tag name.
|
||||
Name string
|
||||
|
||||
// IfdPath is the proper IFD path of this tag. This is not fully-qualified.
|
||||
IfdPath string
|
||||
|
||||
// SupportedTypes is an unsorted list of allowed tag-types.
|
||||
SupportedTypes []exifcommon.TagTypePrimitive
|
||||
}
|
||||
|
||||
// String returns a descriptive string.
|
||||
func (it *IndexedTag) String() string {
|
||||
return fmt.Sprintf("TAG<ID=(0x%04x) NAME=[%s] IFD=[%s]>", it.Id, it.Name, it.IfdPath)
|
||||
}
|
||||
|
||||
// IsName returns true if this tag matches the given tag name.
|
||||
func (it *IndexedTag) IsName(ifdPath, name string) bool {
|
||||
return it.Name == name && it.IfdPath == ifdPath
|
||||
}
|
||||
|
||||
// Is returns true if this tag matched the given tag ID.
|
||||
func (it *IndexedTag) Is(ifdPath string, id uint16) bool {
|
||||
return it.Id == id && it.IfdPath == ifdPath
|
||||
}
|
||||
|
||||
// GetEncodingType returns the largest type that this tag's value can occupy.
|
||||
func (it *IndexedTag) GetEncodingType(value interface{}) exifcommon.TagTypePrimitive {
|
||||
// For convenience, we handle encoding a `time.Time` directly.
|
||||
if IsTime(value) == true {
|
||||
// Timestamps are encoded as ASCII.
|
||||
value = ""
|
||||
}
|
||||
|
||||
if len(it.SupportedTypes) == 0 {
|
||||
log.Panicf("IndexedTag [%s] (%d) has no supported types.", it.IfdPath, it.Id)
|
||||
} else if len(it.SupportedTypes) == 1 {
|
||||
return it.SupportedTypes[0]
|
||||
}
|
||||
|
||||
supportsLong := false
|
||||
supportsShort := false
|
||||
supportsRational := false
|
||||
supportsSignedRational := false
|
||||
for _, supportedType := range it.SupportedTypes {
|
||||
if supportedType == exifcommon.TypeLong {
|
||||
supportsLong = true
|
||||
} else if supportedType == exifcommon.TypeShort {
|
||||
supportsShort = true
|
||||
} else if supportedType == exifcommon.TypeRational {
|
||||
supportsRational = true
|
||||
} else if supportedType == exifcommon.TypeSignedRational {
|
||||
supportsSignedRational = true
|
||||
}
|
||||
}
|
||||
|
||||
// We specifically check for the cases that we know to expect.
|
||||
|
||||
if supportsLong == true && supportsShort == true {
|
||||
return exifcommon.TypeLong
|
||||
} else if supportsRational == true && supportsSignedRational == true {
|
||||
if value == nil {
|
||||
log.Panicf("GetEncodingType: require value to be given")
|
||||
}
|
||||
|
||||
if _, ok := value.(exifcommon.SignedRational); ok == true {
|
||||
return exifcommon.TypeSignedRational
|
||||
}
|
||||
|
||||
return exifcommon.TypeRational
|
||||
}
|
||||
|
||||
log.Panicf("WidestSupportedType() case is not handled for tag [%s] (0x%04x): %v", it.IfdPath, it.Id, it.SupportedTypes)
|
||||
return 0
|
||||
}
|
||||
|
||||
// DoesSupportType returns true if this tag can be found/decoded with this type.
|
||||
func (it *IndexedTag) DoesSupportType(tagType exifcommon.TagTypePrimitive) bool {
|
||||
// This is always a very small collection. So, we keep it unsorted.
|
||||
for _, thisTagType := range it.SupportedTypes {
|
||||
if thisTagType == tagType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// TagIndex is a tag-lookup facility.
|
||||
type TagIndex struct {
|
||||
tagsByIfd map[string]map[uint16]*IndexedTag
|
||||
tagsByIfdR map[string]map[string]*IndexedTag
|
||||
}
|
||||
|
||||
// NewTagIndex returns a new TagIndex struct.
|
||||
func NewTagIndex() *TagIndex {
|
||||
ti := new(TagIndex)
|
||||
|
||||
ti.tagsByIfd = make(map[string]map[uint16]*IndexedTag)
|
||||
ti.tagsByIfdR = make(map[string]map[string]*IndexedTag)
|
||||
|
||||
return ti
|
||||
}
|
||||
|
||||
// Add registers a new tag to be recognized during the parse.
|
||||
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 given a tag ID. `ifdPath` must
|
||||
// not be fully-qualified.
|
||||
func (ti *TagIndex) Get(ii *exifcommon.IfdIdentity, 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)
|
||||
}
|
||||
|
||||
ifdPath := ii.UnindexedString()
|
||||
|
||||
family, found := ti.tagsByIfd[ifdPath]
|
||||
if found == false {
|
||||
return nil, ErrTagNotFound
|
||||
}
|
||||
|
||||
it, found = family[id]
|
||||
if found == false {
|
||||
return nil, ErrTagNotFound
|
||||
}
|
||||
|
||||
return it, nil
|
||||
}
|
||||
|
||||
var (
|
||||
// tagGuessDefaultIfdIdentities describes which IFDs we'll look for a given
|
||||
// tag-ID in, if it's not found where it's supposed to be. We suppose that
|
||||
// Exif-IFD tags might be found in IFD0 or IFD1, or IFD0/IFD1 tags might be
|
||||
// found in the Exif IFD. This is the only thing we've seen so far. So, this
|
||||
// is the limit of our guessing.
|
||||
tagGuessDefaultIfdIdentities = []*exifcommon.IfdIdentity{
|
||||
exifcommon.IfdExifStandardIfdIdentity,
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
}
|
||||
)
|
||||
|
||||
// FindFirst looks for the given tag-ID in each of the given IFDs in the given
|
||||
// order. If `fqIfdPaths` is `nil` then use a default search order. This defies
|
||||
// the standard, which requires each tag to exist in certain IFDs. This is a
|
||||
// contingency to make recommendations for malformed data.
|
||||
//
|
||||
// Things *can* end badly here, in that the same tag-ID in different IFDs might
|
||||
// describe different data and different ata-types, and our decode might then
|
||||
// produce binary and non-printable data.
|
||||
func (ti *TagIndex) FindFirst(id uint16, typeId exifcommon.TagTypePrimitive, ifdIdentities []*exifcommon.IfdIdentity) (it *IndexedTag, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
if ifdIdentities == nil {
|
||||
ifdIdentities = tagGuessDefaultIfdIdentities
|
||||
}
|
||||
|
||||
for _, ii := range ifdIdentities {
|
||||
it, err := ti.Get(ii, id)
|
||||
if err != nil {
|
||||
if err == ErrTagNotFound {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
// Even though the tag might be mislocated, the type should still be the
|
||||
// same. Check this so we don't accidentally end-up on a complete
|
||||
// irrelevant tag with a totally different data type. This attempts to
|
||||
// mitigate producing garbage.
|
||||
for _, supportedType := range it.SupportedTypes {
|
||||
if supportedType == typeId {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrTagNotFound
|
||||
}
|
||||
|
||||
// GetWithName returns information about the non-IFD tag given a tag name.
|
||||
func (ti *TagIndex) GetWithName(ii *exifcommon.IfdIdentity, 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)
|
||||
}
|
||||
|
||||
ifdPath := ii.UnindexedString()
|
||||
|
||||
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
|
||||
tagTypeNames := tagInfo.TypeNames
|
||||
|
||||
if tagTypeNames == nil {
|
||||
if tagTypeName == "" {
|
||||
log.Panicf("no tag-types were given when registering standard tag [%s] (0x%04x) [%s]", ifdPath, tagId, tagName)
|
||||
}
|
||||
|
||||
tagTypeNames = []string{
|
||||
tagTypeName,
|
||||
}
|
||||
} else if tagTypeName != "" {
|
||||
log.Panicf("both 'type_names' and 'type_name' were given when registering standard tag [%s] (0x%04x) [%s]", ifdPath, tagId, tagName)
|
||||
}
|
||||
|
||||
tagTypes := make([]exifcommon.TagTypePrimitive, 0)
|
||||
for _, tagTypeName := range tagTypeNames {
|
||||
|
||||
// TODO(dustin): Discard unsupported types. This helps us with non-standard types that have actually been found in real data, that we ignore for right now. e.g. SSHORT, FLOAT, DOUBLE
|
||||
tagTypeId, found := exifcommon.GetTypeByName(tagTypeName)
|
||||
if found == false {
|
||||
tagsLogger.Warningf(nil, "Type [%s] for tag [%s] being loaded is not valid and is being ignored.", tagTypeName, tagName)
|
||||
continue
|
||||
}
|
||||
|
||||
tagTypes = append(tagTypes, tagTypeId)
|
||||
}
|
||||
|
||||
if len(tagTypes) == 0 {
|
||||
tagsLogger.Warningf(nil, "Tag [%s] (0x%04x) [%s] being loaded does not have any supported types and will not be registered.", ifdPath, tagId, tagName)
|
||||
continue
|
||||
}
|
||||
|
||||
it := &IndexedTag{
|
||||
IfdPath: ifdPath,
|
||||
Id: tagId,
|
||||
Name: tagName,
|
||||
SupportedTypes: tagTypes,
|
||||
}
|
||||
|
||||
err = ti.Add(it)
|
||||
log.PanicIf(err)
|
||||
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
tagsLogger.Debugf(nil, "(%d) tags loaded.", count)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,929 @@
|
|||
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_names: [LONG, SHORT]
|
||||
- id: 0xa003
|
||||
name: PixelYDimension
|
||||
type_names: [LONG, SHORT]
|
||||
- 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_names: [LONG, SHORT]
|
||||
- id: 0x0101
|
||||
name: ImageLength
|
||||
type_names: [LONG, SHORT]
|
||||
- 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_names: [LONG, SHORT]
|
||||
- id: 0x0112
|
||||
name: Orientation
|
||||
type_name: SHORT
|
||||
- id: 0x0115
|
||||
name: SamplesPerPixel
|
||||
type_name: SHORT
|
||||
- id: 0x0116
|
||||
name: RowsPerStrip
|
||||
type_names: [LONG, SHORT]
|
||||
- id: 0x0117
|
||||
name: StripByteCounts
|
||||
type_names: [LONG, SHORT]
|
||||
- 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: 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
|
||||
# NOTE(dustin): SRATIONAL isn't mentioned in the standard, but we have seen it in real data.
|
||||
type_names: [RATIONAL, SRATIONAL]
|
||||
- id: 0x829d
|
||||
name: FNumber
|
||||
# NOTE(dustin): SRATIONAL isn't mentioned in the standard, but we have seen it in real data.
|
||||
type_names: [RATIONAL, SRATIONAL]
|
||||
- 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: 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: 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: 0xc740
|
||||
name: OpcodeList1
|
||||
type_name: UNDEFINED
|
||||
- id: 0xc741
|
||||
name: OpcodeList2
|
||||
type_name: UNDEFINED
|
||||
- id: 0xc74e
|
||||
name: OpcodeList3
|
||||
type_name: UNDEFINED
|
||||
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,403 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestIndexedTag_String(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Id: 0xb,
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
SupportedTypes: []exifcommon.TagTypePrimitive{
|
||||
exifcommon.TagTypePrimitive(11),
|
||||
exifcommon.TagTypePrimitive(22),
|
||||
},
|
||||
}
|
||||
|
||||
if it.String() != "TAG<ID=(0x000b) NAME=[some_name] IFD=[ifd/path]>" {
|
||||
t.Fatalf("String output not correct: [%s]", it.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_IsName_True(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
}
|
||||
|
||||
if it.IsName("ifd/path", "some_name") != true {
|
||||
t.Fatalf("IsName is not true.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_IsName_FalseOnName(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
}
|
||||
|
||||
if it.IsName("ifd/path", "some_name2") != false {
|
||||
t.Fatalf("IsName is not false.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_IsName_FalseOnIfdPath(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
}
|
||||
|
||||
if it.IsName("ifd/path2", "some_name") != false {
|
||||
t.Fatalf("IsName is not false.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_Is_True(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Id: 0x11,
|
||||
IfdPath: "ifd/path",
|
||||
}
|
||||
|
||||
if it.Is("ifd/path", 0x11) != true {
|
||||
t.Fatalf("Is is not true.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_Is_FalseOnId(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Id: 0x11,
|
||||
IfdPath: "ifd/path",
|
||||
}
|
||||
|
||||
if it.Is("ifd/path", 0x12) != false {
|
||||
t.Fatalf("Is is not false.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_Is_FalseOnIfdName(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Id: 0x11,
|
||||
IfdPath: "ifd/path",
|
||||
}
|
||||
|
||||
if it.Is("ifd/path2", 0x11) != false {
|
||||
t.Fatalf("Is is not false.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_GetEncodingType_WorksWithOneType(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Id: 0xb,
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
SupportedTypes: []exifcommon.TagTypePrimitive{
|
||||
exifcommon.TypeRational,
|
||||
},
|
||||
}
|
||||
|
||||
if it.GetEncodingType(nil) != exifcommon.TypeRational {
|
||||
t.Fatalf("Expected the one type that was set.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_GetEncodingType_FailsOnEmpty(t *testing.T) {
|
||||
// This also looks for an empty to reference the first spot, invalidly.
|
||||
|
||||
defer func() {
|
||||
errRaw := recover()
|
||||
if errRaw == nil {
|
||||
t.Fatalf("Expected failure due to empty supported-types.")
|
||||
}
|
||||
|
||||
err := errRaw.(error)
|
||||
if err.Error() != "IndexedTag [] (0) has no supported types." {
|
||||
log.Panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
it := &IndexedTag{
|
||||
SupportedTypes: []exifcommon.TagTypePrimitive{},
|
||||
}
|
||||
|
||||
it.GetEncodingType(nil)
|
||||
}
|
||||
|
||||
func TestIndexedTag_GetEncodingType_PreferLongOverShort(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Id: 0xb,
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
SupportedTypes: []exifcommon.TagTypePrimitive{
|
||||
exifcommon.TypeShort,
|
||||
},
|
||||
}
|
||||
|
||||
if it.GetEncodingType(nil) != exifcommon.TypeShort {
|
||||
t.Fatalf("Expected the second (LONG) type to be returned.")
|
||||
}
|
||||
|
||||
it = &IndexedTag{
|
||||
Id: 0xb,
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
SupportedTypes: []exifcommon.TagTypePrimitive{
|
||||
exifcommon.TypeShort,
|
||||
exifcommon.TypeLong,
|
||||
},
|
||||
}
|
||||
|
||||
if it.GetEncodingType(nil) != exifcommon.TypeLong {
|
||||
t.Fatalf("Expected the second (LONG) type to be returned.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_GetEncodingType_BothRationalTypes(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Id: 0xb,
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
SupportedTypes: []exifcommon.TagTypePrimitive{
|
||||
exifcommon.TypeRational,
|
||||
exifcommon.TypeSignedRational,
|
||||
},
|
||||
}
|
||||
|
||||
v1 := exifcommon.Rational{}
|
||||
|
||||
if it.GetEncodingType(v1) != exifcommon.TypeRational {
|
||||
t.Fatalf("Expected the second (RATIONAL) type to be returned.")
|
||||
}
|
||||
|
||||
v2 := exifcommon.SignedRational{}
|
||||
|
||||
if it.GetEncodingType(v2) != exifcommon.TypeSignedRational {
|
||||
t.Fatalf("Expected the second (SIGNED RATIONAL) type to be returned.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_GetEncodingType_Timestamp(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
SupportedTypes: []exifcommon.TagTypePrimitive{
|
||||
exifcommon.TypeAscii,
|
||||
},
|
||||
}
|
||||
|
||||
zeroTime := time.Time{}
|
||||
|
||||
if it.GetEncodingType(zeroTime) != exifcommon.TypeAscii {
|
||||
t.Fatalf("Expected the timestamp to to be encoded as ASCII.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexedTag_DoesSupportType(t *testing.T) {
|
||||
it := &IndexedTag{
|
||||
Id: 0xb,
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
SupportedTypes: []exifcommon.TagTypePrimitive{
|
||||
exifcommon.TypeRational,
|
||||
exifcommon.TypeSignedRational,
|
||||
},
|
||||
}
|
||||
|
||||
if it.DoesSupportType(exifcommon.TypeRational) != true {
|
||||
t.Fatalf("Does not support unsigned-rational.")
|
||||
} else if it.DoesSupportType(exifcommon.TypeSignedRational) != true {
|
||||
t.Fatalf("Does not support signed-rational.")
|
||||
} else if it.DoesSupportType(exifcommon.TypeLong) != false {
|
||||
t.Fatalf("Does not support long.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTagIndex(t *testing.T) {
|
||||
ti := NewTagIndex()
|
||||
|
||||
if ti.tagsByIfd == nil {
|
||||
t.Fatalf("tagsByIfd is nil.")
|
||||
} else if ti.tagsByIfdR == nil {
|
||||
t.Fatalf("tagsByIfdR is nil.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagIndex_Add(t *testing.T) {
|
||||
ti := NewTagIndex()
|
||||
|
||||
if len(ti.tagsByIfd) != 0 {
|
||||
t.Fatalf("tagsByIfd should be empty initially.")
|
||||
} else if len(ti.tagsByIfdR) != 0 {
|
||||
t.Fatalf("tagsByIfdR should be empty initially.")
|
||||
}
|
||||
|
||||
it := &IndexedTag{
|
||||
Id: 0xb,
|
||||
Name: "some_name",
|
||||
IfdPath: "ifd/path",
|
||||
SupportedTypes: []exifcommon.TagTypePrimitive{
|
||||
exifcommon.TypeRational,
|
||||
exifcommon.TypeSignedRational,
|
||||
},
|
||||
}
|
||||
|
||||
err := ti.Add(it)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(ti.tagsByIfd[it.IfdPath][it.Id], it) != true {
|
||||
t.Fatalf("Not present in forward lookup.")
|
||||
} else if reflect.DeepEqual(ti.tagsByIfdR[it.IfdPath][it.Name], it) != true {
|
||||
t.Fatalf("Not present in reverse lookup.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagIndex_Get(t *testing.T) {
|
||||
ti := NewTagIndex()
|
||||
|
||||
it, err := ti.Get(exifcommon.IfdStandardIfdIdentity, 0x10f)
|
||||
log.PanicIf(err)
|
||||
|
||||
if it.Is(exifcommon.IfdStandardIfdIdentity.UnindexedString(), 0x10f) == false || it.IsName(exifcommon.IfdStandardIfdIdentity.UnindexedString(), "Make") == false {
|
||||
t.Fatalf("tag info not correct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagIndex_GetWithName(t *testing.T) {
|
||||
ti := NewTagIndex()
|
||||
|
||||
it, err := ti.GetWithName(exifcommon.IfdStandardIfdIdentity, "Make")
|
||||
log.PanicIf(err)
|
||||
|
||||
if it.Is(exifcommon.IfdStandardIfdIdentity.UnindexedString(), 0x10f) == false {
|
||||
t.Fatalf("tag info not correct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagIndex_FindFirst_HitOnFirst(t *testing.T) {
|
||||
|
||||
searchOrder := []*exifcommon.IfdIdentity{
|
||||
exifcommon.IfdExifStandardIfdIdentity,
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
}
|
||||
|
||||
ti := NewTagIndex()
|
||||
|
||||
// ExifVersion
|
||||
it, err := ti.FindFirst(0x9000, exifcommon.TypeUndefined, searchOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if it.Is("IFD/Exif", 0x9000) != true {
|
||||
t.Fatalf("Returned tag is not correct.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagIndex_FindFirst_HitOnSecond(t *testing.T) {
|
||||
|
||||
searchOrder := []*exifcommon.IfdIdentity{
|
||||
exifcommon.IfdExifStandardIfdIdentity,
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
}
|
||||
|
||||
ti := NewTagIndex()
|
||||
|
||||
// ProcessingSoftware
|
||||
it, err := ti.FindFirst(0x000b, exifcommon.TypeAscii, searchOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if it.Is("IFD", 0x000b) != true {
|
||||
t.Fatalf("Returned tag is not correct.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagIndex_FindFirst_DefaultOrder_Miss(t *testing.T) {
|
||||
|
||||
searchOrder := []*exifcommon.IfdIdentity{
|
||||
exifcommon.IfdExifStandardIfdIdentity,
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
}
|
||||
|
||||
ti := NewTagIndex()
|
||||
|
||||
_, err := ti.FindFirst(0x1234, exifcommon.TypeRational, searchOrder)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for invalid tag.")
|
||||
} else if err != ErrTagNotFound {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagIndex_FindFirst_ReverseDefaultOrder_HitOnSecond(t *testing.T) {
|
||||
|
||||
reverseSearchOrder := []*exifcommon.IfdIdentity{
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
exifcommon.IfdExifStandardIfdIdentity,
|
||||
}
|
||||
|
||||
ti := NewTagIndex()
|
||||
|
||||
// ExifVersion
|
||||
it, err := ti.FindFirst(0x9000, exifcommon.TypeUndefined, reverseSearchOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if it.Is("IFD/Exif", 0x9000) != true {
|
||||
t.Fatalf("Returned tag is not correct.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagIndex_FindFirst_ReverseDefaultOrder_HitOnFirst(t *testing.T) {
|
||||
|
||||
reverseSearchOrder := []*exifcommon.IfdIdentity{
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
exifcommon.IfdExifStandardIfdIdentity,
|
||||
}
|
||||
|
||||
ti := NewTagIndex()
|
||||
|
||||
// ProcessingSoftware
|
||||
it, err := ti.FindFirst(0x000b, exifcommon.TypeAscii, reverseSearchOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if it.Is("IFD", 0x000b) != true {
|
||||
t.Fatalf("Returned tag is not correct.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagIndex_FindFirst_ReverseDefaultOrder_Miss(t *testing.T) {
|
||||
|
||||
reverseSearchOrder := []*exifcommon.IfdIdentity{
|
||||
exifcommon.IfdStandardIfdIdentity,
|
||||
exifcommon.IfdExifStandardIfdIdentity,
|
||||
}
|
||||
|
||||
ti := NewTagIndex()
|
||||
|
||||
_, err := ti.FindFirst(0x1234, exifcommon.TypeRational, reverseSearchOrder)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for invalid tag.")
|
||||
} else if err != ErrTagNotFound {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadStandardTags(t *testing.T) {
|
||||
ti := NewTagIndex()
|
||||
|
||||
if len(ti.tagsByIfd) != 0 {
|
||||
t.Fatalf("tagsByIfd should be empty initially.")
|
||||
} else if len(ti.tagsByIfdR) != 0 {
|
||||
t.Fatalf("tagsByIfdR should be empty initially.")
|
||||
}
|
||||
|
||||
err := LoadStandardTags(ti)
|
||||
log.PanicIf(err)
|
||||
|
||||
if len(ti.tagsByIfd) == 0 {
|
||||
t.Fatalf("tagsByIfd should be non-empty at the end.")
|
||||
} else if len(ti.tagsByIfdR) == 0 {
|
||||
t.Fatalf("tagsByIfdR should be non-empty at the end.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
var (
|
||||
testExifData []byte
|
||||
)
|
||||
|
||||
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, exifcommon.IfdStandardIfdIdentity, exifcommon.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, []exifcommon.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, exifcommon.IfdStandardIfdIdentity, exifcommon.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, []exifcommon.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 != exifcommon.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 != exifcommon.TestDefaultByteOrder {
|
||||
t.Fatalf("IFD byte-order not correct.")
|
||||
} else if ifd.ifdIdentity.UnindexedString() != exifcommon.IfdStandardIfdIdentity.UnindexedString() {
|
||||
t.Fatalf("IFD name not correct.")
|
||||
} else if ifd.ifdIdentity.Index() != 0 {
|
||||
t.Fatalf("IFD index not zero: (%d)", ifd.ifdIdentity.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, original types (this is awesome).
|
||||
|
||||
expected := []struct {
|
||||
tagId uint16
|
||||
value interface{}
|
||||
}{
|
||||
{tagId: 0x000b, value: "asciivalue"},
|
||||
{tagId: 0x00ff, value: []uint16{0x1122}},
|
||||
{tagId: 0x0100, value: []uint32{0x33445566}},
|
||||
{tagId: 0x013e, value: []exifcommon.Rational{{Numerator: 0x11112222, Denominator: 0x33334444}}},
|
||||
}
|
||||
|
||||
for i, ite := range ifd.Entries {
|
||||
if ite.TagId() != expected[i].tagId {
|
||||
t.Fatalf("Tag-ID for entry (%d) not correct: (0x%02x) != (0x%02x)", i, ite.TagId(), expected[i].tagId)
|
||||
}
|
||||
|
||||
value, err := ite.Value()
|
||||
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 getTestImageFilepath() string {
|
||||
assetsPath := exifcommon.GetTestAssetsPath()
|
||||
testImageFilepath := path.Join(assetsPath, "NDM_8901.jpg")
|
||||
return testImageFilepath
|
||||
}
|
||||
|
||||
func getTestExifData() []byte {
|
||||
if testExifData == nil {
|
||||
assetsPath := exifcommon.GetTestAssetsPath()
|
||||
filepath := path.Join(assetsPath, "NDM_8901.jpg.exif")
|
||||
|
||||
var err error
|
||||
|
||||
testExifData, err = ioutil.ReadFile(filepath)
|
||||
log.PanicIf(err)
|
||||
}
|
||||
|
||||
return testExifData
|
||||
}
|
||||
|
||||
func getTestGpsImageFilepath() string {
|
||||
assetsPath := exifcommon.GetTestAssetsPath()
|
||||
testGpsImageFilepath := path.Join(assetsPath, "gps.jpg")
|
||||
return testGpsImageFilepath
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
## 0xa40b
|
||||
|
||||
The specification is not specific/clear enough to be handled. Without a working example ,we're deferring until some point in the future when either we or someone else has a better understanding.
|
|
@ -0,0 +1,62 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
// Encode encodes the given encodeable undefined value to bytes.
|
||||
func Encode(value EncodeableValue, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
encoderName := value.EncoderName()
|
||||
|
||||
encoder, found := encoders[encoderName]
|
||||
if found == false {
|
||||
log.Panicf("no encoder registered for type [%s]", encoderName)
|
||||
}
|
||||
|
||||
encoded, unitCount, err = encoder.Encode(value, byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
return encoded, unitCount, nil
|
||||
}
|
||||
|
||||
// Decode constructs a value from raw encoded bytes
|
||||
func Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
uth := UndefinedTagHandle{
|
||||
IfdPath: valueContext.IfdPath(),
|
||||
TagId: valueContext.TagId(),
|
||||
}
|
||||
|
||||
decoder, found := decoders[uth]
|
||||
if found == false {
|
||||
// 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, exifcommon.ErrUnhandledUndefinedTypedTag
|
||||
}
|
||||
|
||||
value, err = decoder.Decode(valueContext)
|
||||
if err != nil {
|
||||
if err == ErrUnparseableValue {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type Tag8828Oecf struct {
|
||||
Columns uint16
|
||||
Rows uint16
|
||||
ColumnNames []string
|
||||
Values []exifcommon.SignedRational
|
||||
}
|
||||
|
||||
func (oecf Tag8828Oecf) String() string {
|
||||
return fmt.Sprintf("Tag8828Oecf<COLUMNS=(%d) ROWS=(%d)>", oecf.Columns, oecf.Rows)
|
||||
}
|
||||
|
||||
func (oecf Tag8828Oecf) EncoderName() string {
|
||||
return "Codec8828Oecf"
|
||||
}
|
||||
|
||||
type Codec8828Oecf struct {
|
||||
}
|
||||
|
||||
func (Codec8828Oecf) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test
|
||||
|
||||
oecf, ok := value.(Tag8828Oecf)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a Tag8828Oecf")
|
||||
}
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
|
||||
err = binary.Write(b, byteOrder, oecf.Columns)
|
||||
log.PanicIf(err)
|
||||
|
||||
err = binary.Write(b, byteOrder, oecf.Rows)
|
||||
log.PanicIf(err)
|
||||
|
||||
for _, name := range oecf.ColumnNames {
|
||||
_, err := b.Write([]byte(name))
|
||||
log.PanicIf(err)
|
||||
|
||||
_, err = b.Write([]byte{0})
|
||||
log.PanicIf(err)
|
||||
}
|
||||
|
||||
ve := exifcommon.NewValueEncoder(byteOrder)
|
||||
|
||||
ed, err := ve.Encode(oecf.Values)
|
||||
log.PanicIf(err)
|
||||
|
||||
_, err = b.Write(ed.Encoded)
|
||||
log.PanicIf(err)
|
||||
|
||||
return b.Bytes(), uint32(b.Len()), nil
|
||||
}
|
||||
|
||||
func (Codec8828Oecf) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test using known good data.
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeByte)
|
||||
|
||||
valueBytes, err := valueContext.ReadBytes()
|
||||
log.PanicIf(err)
|
||||
|
||||
oecf := Tag8828Oecf{}
|
||||
|
||||
oecf.Columns = valueContext.ByteOrder().Uint16(valueBytes[0:2])
|
||||
oecf.Rows = valueContext.ByteOrder().Uint16(valueBytes[2:4])
|
||||
|
||||
columnNames := make([]string, oecf.Columns)
|
||||
|
||||
// startAt is where the current column name starts.
|
||||
startAt := 4
|
||||
|
||||
// offset is our current position.
|
||||
offset := startAt
|
||||
|
||||
currentColumnNumber := uint16(0)
|
||||
|
||||
for currentColumnNumber < oecf.Columns {
|
||||
if valueBytes[offset] == 0 {
|
||||
columnName := string(valueBytes[startAt:offset])
|
||||
if len(columnName) == 0 {
|
||||
log.Panicf("SFR column (%d) has zero length", currentColumnNumber)
|
||||
}
|
||||
|
||||
columnNames[currentColumnNumber] = columnName
|
||||
currentColumnNumber++
|
||||
|
||||
offset++
|
||||
startAt = offset
|
||||
continue
|
||||
}
|
||||
|
||||
offset++
|
||||
}
|
||||
|
||||
oecf.ColumnNames = columnNames
|
||||
|
||||
rawRationalBytes := valueBytes[offset:]
|
||||
|
||||
rationalSize := exifcommon.TypeSignedRational.Size()
|
||||
if len(rawRationalBytes)%rationalSize > 0 {
|
||||
log.Panicf("OECF signed-rationals not aligned: (%d) %% (%d) > 0", len(rawRationalBytes), rationalSize)
|
||||
}
|
||||
|
||||
rationalCount := len(rawRationalBytes) / rationalSize
|
||||
|
||||
parser := new(exifcommon.Parser)
|
||||
|
||||
byteOrder := valueContext.ByteOrder()
|
||||
|
||||
items, err := parser.ParseSignedRationals(rawRationalBytes, uint32(rationalCount), byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
oecf.Values = items
|
||||
|
||||
return oecf, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0x8828,
|
||||
Codec8828Oecf{})
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTag8828Oecf_String(t *testing.T) {
|
||||
ut := Tag8828Oecf{
|
||||
Columns: 11,
|
||||
Rows: 22,
|
||||
}
|
||||
|
||||
s := ut.String()
|
||||
|
||||
if s != "Tag8828Oecf<COLUMNS=(11) ROWS=(22)>" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec8828Oecf_Encode(t *testing.T) {
|
||||
ut := Tag8828Oecf{
|
||||
Columns: 2,
|
||||
Rows: 22,
|
||||
ColumnNames: []string{"aa", "bb"},
|
||||
Values: []exifcommon.SignedRational{{11, 22}},
|
||||
}
|
||||
|
||||
codec := Codec8828Oecf{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedBytes := []byte{
|
||||
0x00, 0x02,
|
||||
0x00, 0x16,
|
||||
0x61, 0x61, 0x00, 0x62, 0x62, 0x00,
|
||||
0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x16}
|
||||
|
||||
if bytes.Equal(encoded, expectedBytes) != true {
|
||||
exifcommon.DumpBytesClause(encoded)
|
||||
|
||||
t.Fatalf("Encoded bytes not correct.")
|
||||
} else if unitCount != 18 {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec8828Oecf_Decode(t *testing.T) {
|
||||
encoded := []byte{
|
||||
0x00, 0x02,
|
||||
0x00, 0x16,
|
||||
0x61, 0x61, 0x00, 0x62, 0x62, 0x00,
|
||||
0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x16}
|
||||
|
||||
addressableData := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(encoded)),
|
||||
0,
|
||||
nil,
|
||||
addressableData,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := Codec8828Oecf{}
|
||||
|
||||
value, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedValue := Tag8828Oecf{
|
||||
Columns: 2,
|
||||
Rows: 22,
|
||||
ColumnNames: []string{"aa", "bb"},
|
||||
Values: []exifcommon.SignedRational{{11, 22}},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expectedValue) != true {
|
||||
t.Fatalf("Decoded value not correct: %s", value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type Tag9000ExifVersion struct {
|
||||
ExifVersion string
|
||||
}
|
||||
|
||||
func (Tag9000ExifVersion) EncoderName() string {
|
||||
return "Codec9000ExifVersion"
|
||||
}
|
||||
|
||||
func (ev Tag9000ExifVersion) String() string {
|
||||
return ev.ExifVersion
|
||||
}
|
||||
|
||||
type Codec9000ExifVersion struct {
|
||||
}
|
||||
|
||||
func (Codec9000ExifVersion) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
s, ok := value.(Tag9000ExifVersion)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a Tag9000ExifVersion")
|
||||
}
|
||||
|
||||
return []byte(s.ExifVersion), uint32(len(s.ExifVersion)), nil
|
||||
}
|
||||
|
||||
func (Codec9000ExifVersion) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul)
|
||||
|
||||
valueString, err := valueContext.ReadAsciiNoNul()
|
||||
log.PanicIf(err)
|
||||
|
||||
ev := Tag9000ExifVersion{
|
||||
ExifVersion: valueString,
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
Tag9000ExifVersion{},
|
||||
Codec9000ExifVersion{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0x9000,
|
||||
Codec9000ExifVersion{})
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTag9000ExifVersion_String(t *testing.T) {
|
||||
ut := Tag9000ExifVersion{"abc"}
|
||||
s := ut.String()
|
||||
|
||||
if s != "abc" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec9000ExifVersion_Encode(t *testing.T) {
|
||||
s := "abc"
|
||||
ut := Tag9000ExifVersion{s}
|
||||
|
||||
codec := Codec9000ExifVersion{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(encoded, []byte(s)) != true {
|
||||
t.Fatalf("Encoded bytes not correct: %v", encoded)
|
||||
} else if unitCount != uint32(len(s)) {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec9000ExifVersion_Decode(t *testing.T) {
|
||||
s := "abc"
|
||||
ut := Tag9000ExifVersion{s}
|
||||
|
||||
encoded := []byte(s)
|
||||
|
||||
rawValueOffset := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(encoded)),
|
||||
0,
|
||||
rawValueOffset,
|
||||
nil,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := Codec9000ExifVersion{}
|
||||
|
||||
value, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, ut) != true {
|
||||
t.Fatalf("Decoded value not correct: %s\n", value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
const (
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_Y = 0x1
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_Cb = 0x2
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_Cr = 0x3
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_R = 0x4
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_G = 0x5
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_B = 0x6
|
||||
)
|
||||
|
||||
const (
|
||||
TagUndefinedType_9101_ComponentsConfiguration_OTHER = iota
|
||||
TagUndefinedType_9101_ComponentsConfiguration_RGB = iota
|
||||
TagUndefinedType_9101_ComponentsConfiguration_YCBCR = iota
|
||||
)
|
||||
|
||||
var (
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Names = map[int]string{
|
||||
TagUndefinedType_9101_ComponentsConfiguration_OTHER: "OTHER",
|
||||
TagUndefinedType_9101_ComponentsConfiguration_RGB: "RGB",
|
||||
TagUndefinedType_9101_ComponentsConfiguration_YCBCR: "YCBCR",
|
||||
}
|
||||
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Configurations = map[int][]byte{
|
||||
TagUndefinedType_9101_ComponentsConfiguration_RGB: {
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_R,
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_G,
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_B,
|
||||
0,
|
||||
},
|
||||
|
||||
TagUndefinedType_9101_ComponentsConfiguration_YCBCR: {
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_Y,
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_Cb,
|
||||
TagUndefinedType_9101_ComponentsConfiguration_Channel_Cr,
|
||||
0,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type TagExif9101ComponentsConfiguration struct {
|
||||
ConfigurationId int
|
||||
ConfigurationBytes []byte
|
||||
}
|
||||
|
||||
func (TagExif9101ComponentsConfiguration) EncoderName() string {
|
||||
return "CodecExif9101ComponentsConfiguration"
|
||||
}
|
||||
|
||||
func (cc TagExif9101ComponentsConfiguration) String() string {
|
||||
return fmt.Sprintf("Exif9101ComponentsConfiguration<ID=[%s] BYTES=%v>", TagUndefinedType_9101_ComponentsConfiguration_Names[cc.ConfigurationId], cc.ConfigurationBytes)
|
||||
}
|
||||
|
||||
type CodecExif9101ComponentsConfiguration struct {
|
||||
}
|
||||
|
||||
func (CodecExif9101ComponentsConfiguration) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
cc, ok := value.(TagExif9101ComponentsConfiguration)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a TagExif9101ComponentsConfiguration")
|
||||
}
|
||||
|
||||
return cc.ConfigurationBytes, uint32(len(cc.ConfigurationBytes)), nil
|
||||
}
|
||||
|
||||
func (CodecExif9101ComponentsConfiguration) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeByte)
|
||||
|
||||
valueBytes, err := valueContext.ReadBytes()
|
||||
log.PanicIf(err)
|
||||
|
||||
for configurationId, configurationBytes := range TagUndefinedType_9101_ComponentsConfiguration_Configurations {
|
||||
if bytes.Equal(configurationBytes, valueBytes) == true {
|
||||
cc := TagExif9101ComponentsConfiguration{
|
||||
ConfigurationId: configurationId,
|
||||
ConfigurationBytes: valueBytes,
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
}
|
||||
|
||||
cc := TagExif9101ComponentsConfiguration{
|
||||
ConfigurationId: TagUndefinedType_9101_ComponentsConfiguration_OTHER,
|
||||
ConfigurationBytes: valueBytes,
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
TagExif9101ComponentsConfiguration{},
|
||||
CodecExif9101ComponentsConfiguration{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0x9101,
|
||||
CodecExif9101ComponentsConfiguration{})
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTagExif9101ComponentsConfiguration_String(t *testing.T) {
|
||||
ut := TagExif9101ComponentsConfiguration{
|
||||
ConfigurationId: TagUndefinedType_9101_ComponentsConfiguration_RGB,
|
||||
ConfigurationBytes: []byte{0x11, 0x22, 0x33, 0x44},
|
||||
}
|
||||
|
||||
s := ut.String()
|
||||
|
||||
if s != "Exif9101ComponentsConfiguration<ID=[RGB] BYTES=[17 34 51 68]>" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecExif9101ComponentsConfiguration_Encode(t *testing.T) {
|
||||
configurationBytes := []byte(TagUndefinedType_9101_ComponentsConfiguration_Names[TagUndefinedType_9101_ComponentsConfiguration_RGB])
|
||||
|
||||
ut := TagExif9101ComponentsConfiguration{
|
||||
ConfigurationId: TagUndefinedType_9101_ComponentsConfiguration_RGB,
|
||||
ConfigurationBytes: configurationBytes,
|
||||
}
|
||||
|
||||
codec := CodecExif9101ComponentsConfiguration{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(encoded, configurationBytes) != true {
|
||||
exifcommon.DumpBytesClause(encoded)
|
||||
|
||||
t.Fatalf("Encoded bytes not correct: %v", encoded)
|
||||
} else if unitCount != uint32(len(configurationBytes)) {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
|
||||
s := string(configurationBytes)
|
||||
|
||||
if s != TagUndefinedType_9101_ComponentsConfiguration_Names[TagUndefinedType_9101_ComponentsConfiguration_RGB] {
|
||||
t.Fatalf("Recovered configuration name not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecExif9101ComponentsConfiguration_Decode(t *testing.T) {
|
||||
configurationBytes := TagUndefinedType_9101_ComponentsConfiguration_Configurations[TagUndefinedType_9101_ComponentsConfiguration_RGB]
|
||||
|
||||
ut := TagExif9101ComponentsConfiguration{
|
||||
ConfigurationId: TagUndefinedType_9101_ComponentsConfiguration_RGB,
|
||||
ConfigurationBytes: configurationBytes,
|
||||
}
|
||||
|
||||
rawValueOffset := configurationBytes
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(configurationBytes)),
|
||||
0,
|
||||
rawValueOffset,
|
||||
nil,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := CodecExif9101ComponentsConfiguration{}
|
||||
|
||||
value, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, ut) != true {
|
||||
t.Fatalf("Decoded value not correct: %s != %s\n", value, ut)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type Tag927CMakerNote struct {
|
||||
MakerNoteType []byte
|
||||
MakerNoteBytes []byte
|
||||
}
|
||||
|
||||
func (Tag927CMakerNote) EncoderName() string {
|
||||
return "Codec927CMakerNote"
|
||||
}
|
||||
|
||||
func (mn Tag927CMakerNote) String() string {
|
||||
parts := make([]string, len(mn.MakerNoteType))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type Codec927CMakerNote struct {
|
||||
}
|
||||
|
||||
func (Codec927CMakerNote) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
mn, ok := value.(Tag927CMakerNote)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a Tag927CMakerNote")
|
||||
}
|
||||
|
||||
// TODO(dustin): Confirm this size against the specification.
|
||||
|
||||
return mn.MakerNoteBytes, uint32(len(mn.MakerNoteBytes)), nil
|
||||
}
|
||||
|
||||
func (Codec927CMakerNote) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// 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).
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeByte)
|
||||
|
||||
valueBytes, err := valueContext.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)
|
||||
// }
|
||||
|
||||
var makerNoteType []byte
|
||||
if len(valueBytes) >= 20 {
|
||||
makerNoteType = valueBytes[:20]
|
||||
} else {
|
||||
makerNoteType = valueBytes
|
||||
}
|
||||
|
||||
mn := Tag927CMakerNote{
|
||||
MakerNoteType: makerNoteType,
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
Tag927CMakerNote{},
|
||||
Codec927CMakerNote{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0x927c,
|
||||
Codec927CMakerNote{})
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTag927CMakerNote_String(t *testing.T) {
|
||||
ut := Tag927CMakerNote{
|
||||
MakerNoteType: []byte{0, 1, 2, 3, 4},
|
||||
MakerNoteBytes: []byte{5, 6, 7, 8, 9},
|
||||
}
|
||||
|
||||
s := ut.String()
|
||||
if s != "MakerNote<TYPE-ID=[00 01 02 03 04] LEN=(5) SHA1=[bdb42cb7eb76e64efe49b22369b404c67b0af55a]>" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec927CMakerNote_Encode(t *testing.T) {
|
||||
codec := Codec927CMakerNote{}
|
||||
|
||||
prefix := []byte{0, 1, 2, 3, 4}
|
||||
b := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
ut := Tag927CMakerNote{
|
||||
MakerNoteType: prefix,
|
||||
MakerNoteBytes: b,
|
||||
}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(encoded, b) != true {
|
||||
t.Fatalf("Encoding not correct: %v", encoded)
|
||||
} else if unitCount != uint32(len(b)) {
|
||||
t.Fatalf("Unit-count not correct: (%d)", len(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec927CMakerNote_Decode(t *testing.T) {
|
||||
b := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
ut := Tag927CMakerNote{
|
||||
MakerNoteType: b,
|
||||
MakerNoteBytes: b,
|
||||
}
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(b)),
|
||||
0,
|
||||
nil,
|
||||
b,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := Codec927CMakerNote{}
|
||||
|
||||
value, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, ut) != true {
|
||||
t.Fatalf("Decoded value not correct: %s != %s", value, ut)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
var (
|
||||
exif9286Logger = log.NewLogger("exifundefined.exif_9286_user_comment")
|
||||
)
|
||||
|
||||
const (
|
||||
TagUndefinedType_9286_UserComment_Encoding_ASCII = iota
|
||||
TagUndefinedType_9286_UserComment_Encoding_JIS = iota
|
||||
TagUndefinedType_9286_UserComment_Encoding_UNICODE = iota
|
||||
TagUndefinedType_9286_UserComment_Encoding_UNDEFINED = iota
|
||||
)
|
||||
|
||||
var (
|
||||
TagUndefinedType_9286_UserComment_Encoding_Names = map[int]string{
|
||||
TagUndefinedType_9286_UserComment_Encoding_ASCII: "ASCII",
|
||||
TagUndefinedType_9286_UserComment_Encoding_JIS: "JIS",
|
||||
TagUndefinedType_9286_UserComment_Encoding_UNICODE: "UNICODE",
|
||||
TagUndefinedType_9286_UserComment_Encoding_UNDEFINED: "UNDEFINED",
|
||||
}
|
||||
|
||||
TagUndefinedType_9286_UserComment_Encodings = map[int][]byte{
|
||||
TagUndefinedType_9286_UserComment_Encoding_ASCII: {'A', 'S', 'C', 'I', 'I', 0, 0, 0},
|
||||
TagUndefinedType_9286_UserComment_Encoding_JIS: {'J', 'I', 'S', 0, 0, 0, 0, 0},
|
||||
TagUndefinedType_9286_UserComment_Encoding_UNICODE: {'U', 'n', 'i', 'c', 'o', 'd', 'e', 0},
|
||||
TagUndefinedType_9286_UserComment_Encoding_UNDEFINED: {0, 0, 0, 0, 0, 0, 0, 0},
|
||||
}
|
||||
)
|
||||
|
||||
type Tag9286UserComment struct {
|
||||
EncodingType int
|
||||
EncodingBytes []byte
|
||||
}
|
||||
|
||||
func (Tag9286UserComment) EncoderName() string {
|
||||
return "Codec9286UserComment"
|
||||
}
|
||||
|
||||
func (uc Tag9286UserComment) String() string {
|
||||
var valuePhrase string
|
||||
|
||||
if uc.EncodingType == TagUndefinedType_9286_UserComment_Encoding_ASCII {
|
||||
return fmt.Sprintf("[ASCII] %s", string(uc.EncodingBytes))
|
||||
} else {
|
||||
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), TagUndefinedType_9286_UserComment_Encoding_Names[uc.EncodingType], valuePhrase, len(uc.EncodingBytes))
|
||||
}
|
||||
|
||||
type Codec9286UserComment struct {
|
||||
}
|
||||
|
||||
func (Codec9286UserComment) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
uc, ok := value.(Tag9286UserComment)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a Tag9286UserComment")
|
||||
}
|
||||
|
||||
encodingTypeBytes, found := TagUndefinedType_9286_UserComment_Encodings[uc.EncodingType]
|
||||
if found == false {
|
||||
log.Panicf("encoding-type not valid for unknown-type tag 9286 (UserComment): (%d)", uc.EncodingType)
|
||||
}
|
||||
|
||||
encoded = make([]byte, len(uc.EncodingBytes)+8)
|
||||
|
||||
copy(encoded[:8], encodingTypeBytes)
|
||||
copy(encoded[8:], uc.EncodingBytes)
|
||||
|
||||
// TODO(dustin): Confirm this size against the specification.
|
||||
|
||||
return encoded, uint32(len(encoded)), nil
|
||||
}
|
||||
|
||||
func (Codec9286UserComment) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeByte)
|
||||
|
||||
valueBytes, err := valueContext.ReadBytes()
|
||||
log.PanicIf(err)
|
||||
|
||||
if len(valueBytes) < 8 {
|
||||
return nil, ErrUnparseableValue
|
||||
}
|
||||
|
||||
unknownUc := Tag9286UserComment{
|
||||
EncodingType: TagUndefinedType_9286_UserComment_Encoding_UNDEFINED,
|
||||
EncodingBytes: []byte{},
|
||||
}
|
||||
|
||||
encoding := valueBytes[:8]
|
||||
for encodingIndex, encodingBytes := range TagUndefinedType_9286_UserComment_Encodings {
|
||||
if bytes.Compare(encoding, encodingBytes) == 0 {
|
||||
uc := Tag9286UserComment{
|
||||
EncodingType: encodingIndex,
|
||||
EncodingBytes: valueBytes[8:],
|
||||
}
|
||||
|
||||
return uc, nil
|
||||
}
|
||||
}
|
||||
|
||||
exif9286Logger.Warningf(nil, "User-comment encoding not valid. Returning 'unknown' type (the default).")
|
||||
return unknownUc, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
Tag9286UserComment{},
|
||||
Codec9286UserComment{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0x9286,
|
||||
Codec9286UserComment{})
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTag9286UserComment_String(t *testing.T) {
|
||||
comment := "some comment"
|
||||
|
||||
ut := Tag9286UserComment{
|
||||
EncodingType: TagUndefinedType_9286_UserComment_Encoding_ASCII,
|
||||
EncodingBytes: []byte(comment),
|
||||
}
|
||||
|
||||
s := ut.String()
|
||||
if s != "[ASCII] some comment" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec9286UserComment_Encode(t *testing.T) {
|
||||
comment := "some comment"
|
||||
|
||||
ut := Tag9286UserComment{
|
||||
EncodingType: TagUndefinedType_9286_UserComment_Encoding_ASCII,
|
||||
EncodingBytes: []byte(comment),
|
||||
}
|
||||
|
||||
codec := Codec9286UserComment{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
typeBytes := TagUndefinedType_9286_UserComment_Encodings[TagUndefinedType_9286_UserComment_Encoding_ASCII]
|
||||
if bytes.Equal(encoded[:8], typeBytes) != true {
|
||||
exifcommon.DumpBytesClause(encoded[:8])
|
||||
|
||||
t.Fatalf("Encoding type not correct.")
|
||||
}
|
||||
|
||||
if bytes.Equal(encoded[8:], []byte(comment)) != true {
|
||||
exifcommon.DumpBytesClause(encoded[8:])
|
||||
|
||||
t.Fatalf("Encoded comment not correct.")
|
||||
}
|
||||
|
||||
if unitCount != uint32(len(encoded)) {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
|
||||
exifcommon.DumpBytesClause(encoded)
|
||||
|
||||
}
|
||||
|
||||
func TestCodec9286UserComment_Decode(t *testing.T) {
|
||||
encoded := []byte{
|
||||
0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00,
|
||||
0x73, 0x6f, 0x6d, 0x65, 0x20, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74,
|
||||
}
|
||||
|
||||
addressableBytes := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(encoded)),
|
||||
0,
|
||||
nil,
|
||||
addressableBytes,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := Codec9286UserComment{}
|
||||
|
||||
decoded, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
comment := "some comment"
|
||||
|
||||
expectedUt := Tag9286UserComment{
|
||||
EncodingType: TagUndefinedType_9286_UserComment_Encoding_ASCII,
|
||||
EncodingBytes: []byte(comment),
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(decoded, expectedUt) != true {
|
||||
t.Fatalf("Decoded struct not correct.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type TagA000FlashpixVersion struct {
|
||||
FlashpixVersion string
|
||||
}
|
||||
|
||||
func (TagA000FlashpixVersion) EncoderName() string {
|
||||
return "CodecA000FlashpixVersion"
|
||||
}
|
||||
|
||||
func (fv TagA000FlashpixVersion) String() string {
|
||||
return fv.FlashpixVersion
|
||||
}
|
||||
|
||||
type CodecA000FlashpixVersion struct {
|
||||
}
|
||||
|
||||
func (CodecA000FlashpixVersion) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
s, ok := value.(TagA000FlashpixVersion)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a TagA000FlashpixVersion")
|
||||
}
|
||||
|
||||
return []byte(s.FlashpixVersion), uint32(len(s.FlashpixVersion)), nil
|
||||
}
|
||||
|
||||
func (CodecA000FlashpixVersion) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul)
|
||||
|
||||
valueString, err := valueContext.ReadAsciiNoNul()
|
||||
log.PanicIf(err)
|
||||
|
||||
fv := TagA000FlashpixVersion{
|
||||
FlashpixVersion: valueString,
|
||||
}
|
||||
|
||||
return fv, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
TagA000FlashpixVersion{},
|
||||
CodecA000FlashpixVersion{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0xa000,
|
||||
CodecA000FlashpixVersion{})
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTagA000FlashpixVersion_String(t *testing.T) {
|
||||
versionPhrase := "some version"
|
||||
|
||||
ut := TagA000FlashpixVersion{versionPhrase}
|
||||
|
||||
s := ut.String()
|
||||
if s != versionPhrase {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecA000FlashpixVersion_Encode(t *testing.T) {
|
||||
versionPhrase := "some version"
|
||||
|
||||
ut := TagA000FlashpixVersion{versionPhrase}
|
||||
|
||||
codec := CodecA000FlashpixVersion{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(encoded, []byte(versionPhrase)) != true {
|
||||
exifcommon.DumpBytesClause(encoded)
|
||||
|
||||
t.Fatalf("Encoding not correct.")
|
||||
} else if unitCount != uint32(len(encoded)) {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecA000FlashpixVersion_Decode(t *testing.T) {
|
||||
versionPhrase := "some version"
|
||||
|
||||
expectedUt := TagA000FlashpixVersion{versionPhrase}
|
||||
|
||||
encoded := []byte(versionPhrase)
|
||||
|
||||
addressableBytes := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(encoded)),
|
||||
0,
|
||||
nil,
|
||||
addressableBytes,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := CodecA000FlashpixVersion{}
|
||||
|
||||
decoded, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(decoded, expectedUt) != true {
|
||||
t.Fatalf("Decoded struct not correct.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type TagA20CSpatialFrequencyResponse struct {
|
||||
Columns uint16
|
||||
Rows uint16
|
||||
ColumnNames []string
|
||||
Values []exifcommon.Rational
|
||||
}
|
||||
|
||||
func (TagA20CSpatialFrequencyResponse) EncoderName() string {
|
||||
return "CodecA20CSpatialFrequencyResponse"
|
||||
}
|
||||
|
||||
func (sfr TagA20CSpatialFrequencyResponse) String() string {
|
||||
return fmt.Sprintf("CodecA20CSpatialFrequencyResponse<COLUMNS=(%d) ROWS=(%d)>", sfr.Columns, sfr.Rows)
|
||||
}
|
||||
|
||||
type CodecA20CSpatialFrequencyResponse struct {
|
||||
}
|
||||
|
||||
func (CodecA20CSpatialFrequencyResponse) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test.
|
||||
|
||||
sfr, ok := value.(TagA20CSpatialFrequencyResponse)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a TagA20CSpatialFrequencyResponse")
|
||||
}
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
|
||||
err = binary.Write(b, byteOrder, sfr.Columns)
|
||||
log.PanicIf(err)
|
||||
|
||||
err = binary.Write(b, byteOrder, sfr.Rows)
|
||||
log.PanicIf(err)
|
||||
|
||||
// Write columns.
|
||||
|
||||
for _, name := range sfr.ColumnNames {
|
||||
_, err := b.WriteString(name)
|
||||
log.PanicIf(err)
|
||||
|
||||
err = b.WriteByte(0)
|
||||
log.PanicIf(err)
|
||||
}
|
||||
|
||||
// Write values.
|
||||
|
||||
ve := exifcommon.NewValueEncoder(byteOrder)
|
||||
|
||||
ed, err := ve.Encode(sfr.Values)
|
||||
log.PanicIf(err)
|
||||
|
||||
_, err = b.Write(ed.Encoded)
|
||||
log.PanicIf(err)
|
||||
|
||||
encoded = b.Bytes()
|
||||
|
||||
// TODO(dustin): Confirm this size against the specification.
|
||||
|
||||
return encoded, uint32(len(encoded)), nil
|
||||
}
|
||||
|
||||
func (CodecA20CSpatialFrequencyResponse) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test using known good data.
|
||||
|
||||
byteOrder := valueContext.ByteOrder()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeByte)
|
||||
|
||||
valueBytes, err := valueContext.ReadBytes()
|
||||
log.PanicIf(err)
|
||||
|
||||
sfr := TagA20CSpatialFrequencyResponse{}
|
||||
|
||||
sfr.Columns = byteOrder.Uint16(valueBytes[0:2])
|
||||
sfr.Rows = byteOrder.Uint16(valueBytes[2:4])
|
||||
|
||||
columnNames := make([]string, sfr.Columns)
|
||||
|
||||
// startAt is where the current column name starts.
|
||||
startAt := 4
|
||||
|
||||
// offset is our current position.
|
||||
offset := 4
|
||||
|
||||
currentColumnNumber := uint16(0)
|
||||
|
||||
for currentColumnNumber < sfr.Columns {
|
||||
if valueBytes[offset] == 0 {
|
||||
columnName := string(valueBytes[startAt:offset])
|
||||
if len(columnName) == 0 {
|
||||
log.Panicf("SFR column (%d) has zero length", currentColumnNumber)
|
||||
}
|
||||
|
||||
columnNames[currentColumnNumber] = columnName
|
||||
currentColumnNumber++
|
||||
|
||||
offset++
|
||||
startAt = offset
|
||||
continue
|
||||
}
|
||||
|
||||
offset++
|
||||
}
|
||||
|
||||
sfr.ColumnNames = columnNames
|
||||
|
||||
rawRationalBytes := valueBytes[offset:]
|
||||
|
||||
rationalSize := exifcommon.TypeRational.Size()
|
||||
if len(rawRationalBytes)%rationalSize > 0 {
|
||||
log.Panicf("SFR rationals not aligned: (%d) %% (%d) > 0", len(rawRationalBytes), rationalSize)
|
||||
}
|
||||
|
||||
rationalCount := len(rawRationalBytes) / rationalSize
|
||||
|
||||
parser := new(exifcommon.Parser)
|
||||
|
||||
items, err := parser.ParseRationals(rawRationalBytes, uint32(rationalCount), byteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
sfr.Values = items
|
||||
|
||||
return sfr, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
TagA20CSpatialFrequencyResponse{},
|
||||
CodecA20CSpatialFrequencyResponse{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0xa20c,
|
||||
CodecA20CSpatialFrequencyResponse{})
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTagA20CSpatialFrequencyResponse_String(t *testing.T) {
|
||||
ut := TagA20CSpatialFrequencyResponse{
|
||||
Columns: 2,
|
||||
Rows: 9,
|
||||
ColumnNames: []string{"column1", "column2"},
|
||||
Values: []exifcommon.Rational{
|
||||
{1, 2},
|
||||
{3, 4},
|
||||
},
|
||||
}
|
||||
|
||||
s := ut.String()
|
||||
if s != "CodecA20CSpatialFrequencyResponse<COLUMNS=(2) ROWS=(9)>" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecA20CSpatialFrequencyResponse_Encode(t *testing.T) {
|
||||
ut := TagA20CSpatialFrequencyResponse{
|
||||
Columns: 2,
|
||||
Rows: 9,
|
||||
ColumnNames: []string{"column1", "column2"},
|
||||
Values: []exifcommon.Rational{
|
||||
{1, 2},
|
||||
{3, 4},
|
||||
},
|
||||
}
|
||||
|
||||
codec := CodecA20CSpatialFrequencyResponse{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedEncoded := []byte{
|
||||
0x00, 0x02,
|
||||
0x00, 0x09,
|
||||
0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x31, 0x00,
|
||||
0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x32, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
|
||||
}
|
||||
|
||||
if bytes.Equal(encoded, expectedEncoded) != true {
|
||||
exifcommon.DumpBytesClause(encoded)
|
||||
|
||||
t.Fatalf("Encoding not correct.")
|
||||
} else if unitCount != uint32(len(encoded)) {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecA20CSpatialFrequencyResponse_Decode(t *testing.T) {
|
||||
expectedUt := TagA20CSpatialFrequencyResponse{
|
||||
Columns: 2,
|
||||
Rows: 9,
|
||||
ColumnNames: []string{"column1", "column2"},
|
||||
Values: []exifcommon.Rational{
|
||||
{1, 2},
|
||||
{3, 4},
|
||||
},
|
||||
}
|
||||
|
||||
encoded := []byte{
|
||||
0x00, 0x02,
|
||||
0x00, 0x09,
|
||||
0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x31, 0x00,
|
||||
0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x32, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
|
||||
}
|
||||
|
||||
addressableBytes := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(encoded)),
|
||||
0,
|
||||
nil,
|
||||
addressableBytes,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := CodecA20CSpatialFrequencyResponse{}
|
||||
|
||||
decoded, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(decoded, expectedUt) != true {
|
||||
t.Fatalf("Decoded struct not correct.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type TagExifA300FileSource uint32
|
||||
|
||||
func (TagExifA300FileSource) EncoderName() string {
|
||||
return "CodecExifA300FileSource"
|
||||
}
|
||||
|
||||
func (af TagExifA300FileSource) String() string {
|
||||
return fmt.Sprintf("0x%08x", uint32(af))
|
||||
}
|
||||
|
||||
const (
|
||||
TagUndefinedType_A300_SceneType_Others TagExifA300FileSource = 0
|
||||
TagUndefinedType_A300_SceneType_ScannerOfTransparentType TagExifA300FileSource = 1
|
||||
TagUndefinedType_A300_SceneType_ScannerOfReflexType TagExifA300FileSource = 2
|
||||
TagUndefinedType_A300_SceneType_Dsc TagExifA300FileSource = 3
|
||||
)
|
||||
|
||||
type CodecExifA300FileSource struct {
|
||||
}
|
||||
|
||||
func (CodecExifA300FileSource) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
st, ok := value.(TagExifA300FileSource)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a TagExifA300FileSource")
|
||||
}
|
||||
|
||||
ve := exifcommon.NewValueEncoder(byteOrder)
|
||||
|
||||
ed, err := ve.Encode([]uint32{uint32(st)})
|
||||
log.PanicIf(err)
|
||||
|
||||
// TODO(dustin): Confirm this size against the specification. It's non-specific about what type it is, but it looks to be no more than a single integer scalar. So, we're assuming it's a LONG.
|
||||
|
||||
return ed.Encoded, 1, nil
|
||||
}
|
||||
|
||||
func (CodecExifA300FileSource) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeLong)
|
||||
|
||||
valueLongs, err := valueContext.ReadLongs()
|
||||
log.PanicIf(err)
|
||||
|
||||
return TagExifA300FileSource(valueLongs[0]), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
TagExifA300FileSource(0),
|
||||
CodecExifA300FileSource{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0xa300,
|
||||
CodecExifA300FileSource{})
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTagExifA300FileSource_String(t *testing.T) {
|
||||
ut := TagExifA300FileSource(0x1234)
|
||||
|
||||
s := ut.String()
|
||||
if s != "0x00001234" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecExifA300FileSource_Encode(t *testing.T) {
|
||||
ut := TagExifA300FileSource(0x1234)
|
||||
|
||||
codec := CodecExifA300FileSource{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedEncoded := []byte{0, 0, 0x12, 0x34}
|
||||
|
||||
if bytes.Equal(encoded, expectedEncoded) != true {
|
||||
exifcommon.DumpBytesClause(encoded)
|
||||
|
||||
t.Fatalf("Encoding not correct.")
|
||||
} else if unitCount != 1 {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecExifA300FileSource_Decode(t *testing.T) {
|
||||
expectedUt := TagExifA300FileSource(0x1234)
|
||||
|
||||
encoded := []byte{0, 0, 0x12, 0x34}
|
||||
|
||||
rawValueOffset := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
rawValueOffset,
|
||||
nil,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := CodecExifA300FileSource{}
|
||||
|
||||
decoded, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(decoded, expectedUt) != true {
|
||||
t.Fatalf("Decoded struct not correct.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type TagExifA301SceneType uint32
|
||||
|
||||
func (TagExifA301SceneType) EncoderName() string {
|
||||
return "CodecExifA301SceneType"
|
||||
}
|
||||
|
||||
func (st TagExifA301SceneType) String() string {
|
||||
return fmt.Sprintf("0x%08x", uint32(st))
|
||||
}
|
||||
|
||||
const (
|
||||
TagUndefinedType_A301_SceneType_DirectlyPhotographedImage TagExifA301SceneType = 1
|
||||
)
|
||||
|
||||
type CodecExifA301SceneType struct {
|
||||
}
|
||||
|
||||
func (CodecExifA301SceneType) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
st, ok := value.(TagExifA301SceneType)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a TagExif9101ComponentsConfiguration")
|
||||
}
|
||||
|
||||
ve := exifcommon.NewValueEncoder(byteOrder)
|
||||
|
||||
ed, err := ve.Encode([]uint32{uint32(st)})
|
||||
log.PanicIf(err)
|
||||
|
||||
// TODO(dustin): Confirm this size against the specification. It's non-specific about what type it is, but it looks to be no more than a single integer scalar. So, we're assuming it's a LONG.
|
||||
|
||||
return ed.Encoded, uint32(int(ed.UnitCount)), nil
|
||||
}
|
||||
|
||||
func (CodecExifA301SceneType) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeLong)
|
||||
|
||||
valueLongs, err := valueContext.ReadLongs()
|
||||
log.PanicIf(err)
|
||||
|
||||
return TagExifA301SceneType(valueLongs[0]), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
TagExifA301SceneType(0),
|
||||
CodecExifA301SceneType{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0xa301,
|
||||
CodecExifA301SceneType{})
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTagExifA301SceneType_String(t *testing.T) {
|
||||
ut := TagExifA301SceneType(0x1234)
|
||||
|
||||
s := ut.String()
|
||||
if s != "0x00001234" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecExifA301SceneType_Encode(t *testing.T) {
|
||||
ut := TagExifA301SceneType(0x1234)
|
||||
|
||||
codec := CodecExifA301SceneType{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedEncoded := []byte{0, 0, 0x12, 0x34}
|
||||
|
||||
if bytes.Equal(encoded, expectedEncoded) != true {
|
||||
exifcommon.DumpBytesClause(encoded)
|
||||
|
||||
t.Fatalf("Encoding not correct.")
|
||||
} else if unitCount != 1 {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecExifA301SceneType_Decode(t *testing.T) {
|
||||
expectedUt := TagExifA301SceneType(0x1234)
|
||||
|
||||
encoded := []byte{0, 0, 0x12, 0x34}
|
||||
|
||||
rawValueOffset := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
rawValueOffset,
|
||||
nil,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := CodecExifA301SceneType{}
|
||||
|
||||
decoded, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(decoded, expectedUt) != true {
|
||||
t.Fatalf("Decoded struct not correct.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type TagA302CfaPattern struct {
|
||||
HorizontalRepeat uint16
|
||||
VerticalRepeat uint16
|
||||
CfaValue []byte
|
||||
}
|
||||
|
||||
func (TagA302CfaPattern) EncoderName() string {
|
||||
return "CodecA302CfaPattern"
|
||||
}
|
||||
|
||||
func (cp TagA302CfaPattern) String() string {
|
||||
return fmt.Sprintf("TagA302CfaPattern<HORZ-REPEAT=(%d) VERT-REPEAT=(%d) CFA-VALUE=(%d)>", cp.HorizontalRepeat, cp.VerticalRepeat, len(cp.CfaValue))
|
||||
}
|
||||
|
||||
type CodecA302CfaPattern struct {
|
||||
}
|
||||
|
||||
func (CodecA302CfaPattern) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test.
|
||||
|
||||
cp, ok := value.(TagA302CfaPattern)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a TagA302CfaPattern")
|
||||
}
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
|
||||
err = binary.Write(b, byteOrder, cp.HorizontalRepeat)
|
||||
log.PanicIf(err)
|
||||
|
||||
err = binary.Write(b, byteOrder, cp.VerticalRepeat)
|
||||
log.PanicIf(err)
|
||||
|
||||
_, err = b.Write(cp.CfaValue)
|
||||
log.PanicIf(err)
|
||||
|
||||
encoded = b.Bytes()
|
||||
|
||||
// TODO(dustin): Confirm this size against the specification.
|
||||
|
||||
return encoded, uint32(len(encoded)), nil
|
||||
}
|
||||
|
||||
func (CodecA302CfaPattern) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO(dustin): Add test using known good data.
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeByte)
|
||||
|
||||
valueBytes, err := valueContext.ReadBytes()
|
||||
log.PanicIf(err)
|
||||
|
||||
cp := TagA302CfaPattern{}
|
||||
|
||||
cp.HorizontalRepeat = valueContext.ByteOrder().Uint16(valueBytes[0:2])
|
||||
cp.VerticalRepeat = valueContext.ByteOrder().Uint16(valueBytes[2:4])
|
||||
|
||||
expectedLength := int(cp.HorizontalRepeat * cp.VerticalRepeat)
|
||||
cp.CfaValue = valueBytes[4 : 4+expectedLength]
|
||||
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
TagA302CfaPattern{},
|
||||
CodecA302CfaPattern{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifStandardIfdIdentity.UnindexedString(),
|
||||
0xa302,
|
||||
CodecA302CfaPattern{})
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
func TestTagA302CfaPattern_String(t *testing.T) {
|
||||
ut := TagA302CfaPattern{
|
||||
HorizontalRepeat: 2,
|
||||
VerticalRepeat: 3,
|
||||
CfaValue: []byte{
|
||||
0, 1, 2, 3, 4, 5,
|
||||
},
|
||||
}
|
||||
|
||||
s := ut.String()
|
||||
|
||||
if s != "TagA302CfaPattern<HORZ-REPEAT=(2) VERT-REPEAT=(3) CFA-VALUE=(6)>" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecA302CfaPattern_Encode(t *testing.T) {
|
||||
ut := TagA302CfaPattern{
|
||||
HorizontalRepeat: 2,
|
||||
VerticalRepeat: 3,
|
||||
CfaValue: []byte{
|
||||
0, 1, 2, 3, 4,
|
||||
5, 6, 7, 8, 9,
|
||||
10, 11, 12, 13, 14,
|
||||
},
|
||||
}
|
||||
|
||||
codec := CodecA302CfaPattern{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedBytes := []byte{
|
||||
0x00, 0x02,
|
||||
0x00, 0x03,
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
}
|
||||
|
||||
if bytes.Equal(encoded, expectedBytes) != true {
|
||||
exifcommon.DumpBytesClause(encoded)
|
||||
|
||||
t.Fatalf("Encoded bytes not correct.")
|
||||
} else if unitCount != 19 {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecA302CfaPattern_Decode(t *testing.T) {
|
||||
encoded := []byte{
|
||||
0x00, 0x02,
|
||||
0x00, 0x03,
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
||||
}
|
||||
|
||||
addressableData := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(encoded)),
|
||||
0,
|
||||
nil,
|
||||
addressableData,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := CodecA302CfaPattern{}
|
||||
|
||||
value, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
expectedValue := TagA302CfaPattern{
|
||||
HorizontalRepeat: 2,
|
||||
VerticalRepeat: 3,
|
||||
CfaValue: []byte{
|
||||
0, 1, 2, 3, 4, 5,
|
||||
},
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(value, expectedValue) != true {
|
||||
t.Fatalf("Decoded value not correct: %s", value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type Tag0002InteropVersion struct {
|
||||
InteropVersion string
|
||||
}
|
||||
|
||||
func (Tag0002InteropVersion) EncoderName() string {
|
||||
return "Codec0002InteropVersion"
|
||||
}
|
||||
|
||||
func (iv Tag0002InteropVersion) String() string {
|
||||
return iv.InteropVersion
|
||||
}
|
||||
|
||||
type Codec0002InteropVersion struct {
|
||||
}
|
||||
|
||||
func (Codec0002InteropVersion) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
s, ok := value.(Tag0002InteropVersion)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a Tag0002InteropVersion")
|
||||
}
|
||||
|
||||
return []byte(s.InteropVersion), uint32(len(s.InteropVersion)), nil
|
||||
}
|
||||
|
||||
func (Codec0002InteropVersion) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul)
|
||||
|
||||
valueString, err := valueContext.ReadAsciiNoNul()
|
||||
log.PanicIf(err)
|
||||
|
||||
iv := Tag0002InteropVersion{
|
||||
InteropVersion: valueString,
|
||||
}
|
||||
|
||||
return iv, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
Tag0002InteropVersion{},
|
||||
Codec0002InteropVersion{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdExifIopStandardIfdIdentity.UnindexedString(),
|
||||
0x0002,
|
||||
Codec0002InteropVersion{})
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTag0002InteropVersion_String(t *testing.T) {
|
||||
ut := Tag0002InteropVersion{"abc"}
|
||||
s := ut.String()
|
||||
|
||||
if s != "abc" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec0002InteropVersion_Encode(t *testing.T) {
|
||||
s := "abc"
|
||||
ut := Tag0002InteropVersion{s}
|
||||
|
||||
codec := Codec0002InteropVersion{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(encoded, []byte(s)) != true {
|
||||
t.Fatalf("Encoded bytes not correct: %v", encoded)
|
||||
} else if unitCount != uint32(len(s)) {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec0002InteropVersion_Decode(t *testing.T) {
|
||||
s := "abc"
|
||||
ut := Tag0002InteropVersion{s}
|
||||
|
||||
encoded := []byte(s)
|
||||
|
||||
rawValueOffset := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(encoded)),
|
||||
0,
|
||||
rawValueOffset,
|
||||
nil,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := Codec0002InteropVersion{}
|
||||
|
||||
value, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, ut) != true {
|
||||
t.Fatalf("Decoded value not correct: %s\n", value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type Tag001BGPSProcessingMethod struct {
|
||||
string
|
||||
}
|
||||
|
||||
func (Tag001BGPSProcessingMethod) EncoderName() string {
|
||||
return "Codec001BGPSProcessingMethod"
|
||||
}
|
||||
|
||||
func (gpm Tag001BGPSProcessingMethod) String() string {
|
||||
return gpm.string
|
||||
}
|
||||
|
||||
type Codec001BGPSProcessingMethod struct {
|
||||
}
|
||||
|
||||
func (Codec001BGPSProcessingMethod) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
s, ok := value.(Tag001BGPSProcessingMethod)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a Tag001BGPSProcessingMethod")
|
||||
}
|
||||
|
||||
return []byte(s.string), uint32(len(s.string)), nil
|
||||
}
|
||||
|
||||
func (Codec001BGPSProcessingMethod) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul)
|
||||
|
||||
valueString, err := valueContext.ReadAsciiNoNul()
|
||||
log.PanicIf(err)
|
||||
|
||||
return Tag001BGPSProcessingMethod{valueString}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
Tag001BGPSProcessingMethod{},
|
||||
Codec001BGPSProcessingMethod{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(),
|
||||
0x001b,
|
||||
Codec001BGPSProcessingMethod{})
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTag001BGPSProcessingMethod_String(t *testing.T) {
|
||||
ut := Tag001BGPSProcessingMethod{"abc"}
|
||||
s := ut.String()
|
||||
|
||||
if s != "abc" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec001BGPSProcessingMethod_Encode(t *testing.T) {
|
||||
s := "abc"
|
||||
ut := Tag001BGPSProcessingMethod{s}
|
||||
|
||||
codec := Codec001BGPSProcessingMethod{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(encoded, []byte(s)) != true {
|
||||
t.Fatalf("Encoded bytes not correct: %v", encoded)
|
||||
} else if unitCount != uint32(len(s)) {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec001BGPSProcessingMethod_Decode(t *testing.T) {
|
||||
s := "abc"
|
||||
ut := Tag001BGPSProcessingMethod{s}
|
||||
|
||||
encoded := []byte(s)
|
||||
|
||||
rawValueOffset := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(encoded)),
|
||||
0,
|
||||
rawValueOffset,
|
||||
nil,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := Codec001BGPSProcessingMethod{}
|
||||
|
||||
value, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, ut) != true {
|
||||
t.Fatalf("Decoded value not correct: %s\n", value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
type Tag001CGPSAreaInformation struct {
|
||||
string
|
||||
}
|
||||
|
||||
func (Tag001CGPSAreaInformation) EncoderName() string {
|
||||
return "Codec001CGPSAreaInformation"
|
||||
}
|
||||
|
||||
func (gai Tag001CGPSAreaInformation) String() string {
|
||||
return gai.string
|
||||
}
|
||||
|
||||
type Codec001CGPSAreaInformation struct {
|
||||
}
|
||||
|
||||
func (Codec001CGPSAreaInformation) Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
s, ok := value.(Tag001CGPSAreaInformation)
|
||||
if ok == false {
|
||||
log.Panicf("can only encode a Tag001CGPSAreaInformation")
|
||||
}
|
||||
|
||||
return []byte(s.string), uint32(len(s.string)), nil
|
||||
}
|
||||
|
||||
func (Codec001CGPSAreaInformation) Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
valueContext.SetUndefinedValueType(exifcommon.TypeAsciiNoNul)
|
||||
|
||||
valueString, err := valueContext.ReadAsciiNoNul()
|
||||
log.PanicIf(err)
|
||||
|
||||
return Tag001CGPSAreaInformation{valueString}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerEncoder(
|
||||
Tag001CGPSAreaInformation{},
|
||||
Codec001CGPSAreaInformation{})
|
||||
|
||||
registerDecoder(
|
||||
exifcommon.IfdGpsInfoStandardIfdIdentity.UnindexedString(),
|
||||
0x001c,
|
||||
Codec001CGPSAreaInformation{})
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
func TestTag001CGPSAreaInformation_String(t *testing.T) {
|
||||
ut := Tag001CGPSAreaInformation{"abc"}
|
||||
s := ut.String()
|
||||
|
||||
if s != "abc" {
|
||||
t.Fatalf("String not correct: [%s]", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec001CGPSAreaInformation_Encode(t *testing.T) {
|
||||
s := "abc"
|
||||
ut := Tag001CGPSAreaInformation{s}
|
||||
|
||||
codec := Codec001CGPSAreaInformation{}
|
||||
|
||||
encoded, unitCount, err := codec.Encode(ut, exifcommon.TestDefaultByteOrder)
|
||||
log.PanicIf(err)
|
||||
|
||||
if bytes.Equal(encoded, []byte(s)) != true {
|
||||
t.Fatalf("Encoded bytes not correct: %v", encoded)
|
||||
} else if unitCount != uint32(len(s)) {
|
||||
t.Fatalf("Unit-count not correct: (%d)", unitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodec001CGPSAreaInformation_Decode(t *testing.T) {
|
||||
s := "abc"
|
||||
ut := Tag001CGPSAreaInformation{s}
|
||||
|
||||
encoded := []byte(s)
|
||||
|
||||
rawValueOffset := encoded
|
||||
|
||||
valueContext := exifcommon.NewValueContext(
|
||||
"",
|
||||
0,
|
||||
uint32(len(encoded)),
|
||||
0,
|
||||
rawValueOffset,
|
||||
nil,
|
||||
exifcommon.TypeUndefined,
|
||||
exifcommon.TestDefaultByteOrder)
|
||||
|
||||
codec := Codec001CGPSAreaInformation{}
|
||||
|
||||
value, err := codec.Decode(valueContext)
|
||||
log.PanicIf(err)
|
||||
|
||||
if reflect.DeepEqual(value, ut) != true {
|
||||
t.Fatalf("Decoded value not correct: %s\n", value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
// UndefinedTagHandle defines one undefined-type tag with a corresponding
|
||||
// decoder.
|
||||
type UndefinedTagHandle struct {
|
||||
IfdPath string
|
||||
TagId uint16
|
||||
}
|
||||
|
||||
func registerEncoder(entity EncodeableValue, encoder UndefinedValueEncoder) {
|
||||
typeName := entity.EncoderName()
|
||||
|
||||
_, found := encoders[typeName]
|
||||
if found == true {
|
||||
log.Panicf("encoder already registered: %v", typeName)
|
||||
}
|
||||
|
||||
encoders[typeName] = encoder
|
||||
}
|
||||
|
||||
func registerDecoder(ifdPath string, tagId uint16, decoder UndefinedValueDecoder) {
|
||||
uth := UndefinedTagHandle{
|
||||
IfdPath: ifdPath,
|
||||
TagId: tagId,
|
||||
}
|
||||
|
||||
_, found := decoders[uth]
|
||||
if found == true {
|
||||
log.Panicf("decoder already registered: %v", uth)
|
||||
}
|
||||
|
||||
decoders[uth] = decoder
|
||||
}
|
||||
|
||||
var (
|
||||
encoders = make(map[string]UndefinedValueEncoder)
|
||||
decoders = make(map[UndefinedTagHandle]UndefinedValueDecoder)
|
||||
)
|
|
@ -0,0 +1,44 @@
|
|||
package exifundefined
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
)
|
||||
|
||||
const (
|
||||
// UnparseableUnknownTagValuePlaceholder is the string to use for an unknown
|
||||
// undefined tag.
|
||||
UnparseableUnknownTagValuePlaceholder = "!UNKNOWN"
|
||||
|
||||
// UnparseableHandledTagValuePlaceholder is the string to use for a known
|
||||
// value that is not parseable.
|
||||
UnparseableHandledTagValuePlaceholder = "!MALFORMED"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnparseableValue is the error for a value that we should have been
|
||||
// able to parse but were not able to.
|
||||
ErrUnparseableValue = errors.New("unparseable undefined tag")
|
||||
)
|
||||
|
||||
// UndefinedValueEncoder knows how to encode an undefined-type tag's value to
|
||||
// bytes.
|
||||
type UndefinedValueEncoder interface {
|
||||
Encode(value interface{}, byteOrder binary.ByteOrder) (encoded []byte, unitCount uint32, err error)
|
||||
}
|
||||
|
||||
// EncodeableValue wraps a value with the information that will be needed to re-
|
||||
// encode it later.
|
||||
type EncodeableValue interface {
|
||||
EncoderName() string
|
||||
String() string
|
||||
}
|
||||
|
||||
// UndefinedValueDecoder knows how to decode an undefined-type tag's value from
|
||||
// bytes.
|
||||
type UndefinedValueDecoder interface {
|
||||
Decode(valueContext *exifcommon.ValueContext) (value EncodeableValue, err error)
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
|
||||
"github.com/dsoprea/go-exif/v2/common"
|
||||
"github.com/dsoprea/go-exif/v2/undefined"
|
||||
)
|
||||
|
||||
var (
|
||||
utilityLogger = log.NewLogger("exif.utility")
|
||||
)
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeOf(time.Time{})
|
||||
)
|
||||
|
||||
// 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) {
|
||||
|
||||
// RELEASE(dustin): Dump this for the next release. It duplicates the same function now in exifcommon.
|
||||
|
||||
return exifcommon.ExifFullTimestampString(t)
|
||||
}
|
||||
|
||||
// ExifTag is one simple representation of a tag in a flat list of all of them.
|
||||
type ExifTag struct {
|
||||
// IfdPath is the fully-qualified IFD path (even though it is not named as
|
||||
// such).
|
||||
IfdPath string `json:"ifd_path"`
|
||||
|
||||
// TagId is the tag-ID.
|
||||
TagId uint16 `json:"id"`
|
||||
|
||||
// TagName is the tag-name. This is never empty.
|
||||
TagName string `json:"name"`
|
||||
|
||||
// UnitCount is the recorded number of units constution of the value.
|
||||
UnitCount uint32 `json:"unit_count"`
|
||||
|
||||
// TagTypeId is the type-ID.
|
||||
TagTypeId exifcommon.TagTypePrimitive `json:"type_id"`
|
||||
|
||||
// TagTypeName is the type name.
|
||||
TagTypeName string `json:"type_name"`
|
||||
|
||||
// Value is the decoded value.
|
||||
Value interface{} `json:"value"`
|
||||
|
||||
// ValueBytes is the raw, encoded value.
|
||||
ValueBytes []byte `json:"value_bytes"`
|
||||
|
||||
// Formatted is the human representation of the first value (tag values are
|
||||
// always an array).
|
||||
FormattedFirst string `json:"formatted_first"`
|
||||
|
||||
// Formatted is the human representation of the complete value.
|
||||
Formatted string `json:"formatted"`
|
||||
|
||||
// ChildIfdPath is the name of the child IFD this tag represents (if it
|
||||
// represents any). Otherwise, this is empty.
|
||||
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.FormattedFirst,
|
||||
len(et.ValueBytes), et.ChildIfdPath)
|
||||
}
|
||||
|
||||
// RELEASE(dustin): In the next release, add an options struct to Scan() and GetFlatExifData(), and put the MiscellaneousExifData in the return.
|
||||
|
||||
// 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))
|
||||
}
|
||||
}()
|
||||
|
||||
eh, err := ParseExifHeader(exifData)
|
||||
log.PanicIf(err)
|
||||
|
||||
im := NewIfdMappingWithStandard()
|
||||
ti := NewTagIndex()
|
||||
|
||||
ie := NewIfdEnumerate(im, ti, exifData, eh.ByteOrder)
|
||||
|
||||
exifTags = make([]ExifTag, 0)
|
||||
|
||||
visitor := func(fqIfdPath string, ifdIndex int, ite *IfdTagEntry) (err error) {
|
||||
// This encodes down to base64. Since this an example tool and we do not
|
||||
// expect to ever decode the output, we are not worried about
|
||||
// specifically base64-encoding it in order to have a measure of
|
||||
// control.
|
||||
valueBytes, err := ite.GetRawBytes()
|
||||
if err != nil {
|
||||
if err == exifundefined.ErrUnparseableValue {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
value, err := ite.Value()
|
||||
if err != nil {
|
||||
if err == exifcommon.ErrUnhandledUndefinedTypedTag {
|
||||
value = exifundefined.UnparseableUnknownTagValuePlaceholder
|
||||
} else {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
et := ExifTag{
|
||||
IfdPath: fqIfdPath,
|
||||
TagId: ite.TagId(),
|
||||
TagName: ite.TagName(),
|
||||
UnitCount: ite.UnitCount(),
|
||||
TagTypeId: ite.TagType(),
|
||||
TagTypeName: ite.TagType().String(),
|
||||
Value: value,
|
||||
ValueBytes: valueBytes,
|
||||
ChildIfdPath: ite.ChildIfdPath(),
|
||||
}
|
||||
|
||||
et.Formatted, err = ite.Format()
|
||||
log.PanicIf(err)
|
||||
|
||||
et.FormattedFirst, err = ite.FormatFirst()
|
||||
log.PanicIf(err)
|
||||
|
||||
exifTags = append(exifTags, et)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = ie.Scan(exifcommon.IfdStandardIfdIdentity, eh.FirstIfdOffset, visitor)
|
||||
log.PanicIf(err)
|
||||
|
||||
return exifTags, nil
|
||||
}
|
||||
|
||||
// GpsDegreesEquals returns true if the two `GpsDegrees` are identical.
|
||||
func GpsDegreesEquals(gi1, gi2 GpsDegrees) bool {
|
||||
if gi2.Orientation != gi1.Orientation {
|
||||
return false
|
||||
}
|
||||
|
||||
degreesRightBound := math.Nextafter(gi1.Degrees, gi1.Degrees+1)
|
||||
minutesRightBound := math.Nextafter(gi1.Minutes, gi1.Minutes+1)
|
||||
secondsRightBound := math.Nextafter(gi1.Seconds, gi1.Seconds+1)
|
||||
|
||||
if gi2.Degrees < gi1.Degrees || gi2.Degrees >= degreesRightBound {
|
||||
return false
|
||||
} else if gi2.Minutes < gi1.Minutes || gi2.Minutes >= minutesRightBound {
|
||||
return false
|
||||
} else if gi2.Seconds < gi1.Seconds || gi2.Seconds >= secondsRightBound {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsTime returns true if the value is a `time.Time`.
|
||||
func IsTime(v interface{}) bool {
|
||||
return reflect.TypeOf(v) == timeType
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
func TestGpsDegreesEquals_Equals(t *testing.T) {
|
||||
gi := GpsDegrees{
|
||||
Orientation: 'A',
|
||||
Degrees: 11.0,
|
||||
Minutes: 22.0,
|
||||
Seconds: 33.0,
|
||||
}
|
||||
|
||||
r := GpsDegreesEquals(gi, gi)
|
||||
if r != true {
|
||||
t.Fatalf("GpsDegrees structs were not equal as expected.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGpsDegreesEquals_NotEqual_Orientation(t *testing.T) {
|
||||
gi1 := GpsDegrees{
|
||||
Orientation: 'A',
|
||||
Degrees: 11.0,
|
||||
Minutes: 22.0,
|
||||
Seconds: 33.0,
|
||||
}
|
||||
|
||||
gi2 := gi1
|
||||
gi2.Orientation = 'B'
|
||||
|
||||
r := GpsDegreesEquals(gi1, gi2)
|
||||
if r != false {
|
||||
t.Fatalf("GpsDegrees structs were equal but not supposed to be.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGpsDegreesEquals_NotEqual_Position(t *testing.T) {
|
||||
gi1 := GpsDegrees{
|
||||
Orientation: 'A',
|
||||
Degrees: 11.0,
|
||||
Minutes: 22.0,
|
||||
Seconds: 33.0,
|
||||
}
|
||||
|
||||
gi2 := gi1
|
||||
gi2.Minutes = 22.5
|
||||
|
||||
r := GpsDegreesEquals(gi1, gi2)
|
||||
if r != false {
|
||||
t.Fatalf("GpsDegrees structs were equal but not supposed to be.")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue