Skip to content

Instantly share code, notes, and snippets.

@pebbie
Created July 20, 2018 12:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pebbie/5b3add920902af7fbfbb4cc6c42eb7ef to your computer and use it in GitHub Desktop.
Save pebbie/5b3add920902af7fbfbb4cc6c42eb7ef to your computer and use it in GitHub Desktop.
hopefully generic TIFF file format extraction tool (GeoTIFF, NEF, ARW)
"""
\description TIFF file format dump
\author paryan
"""
from __future__ import print_function
import argparse
from struct import *
import os
import os.path as path
#
# field (data) type
# code -> (byte size, python struct module unpack code)
#
vtypes = {
12:(8,'d'), # DOUBLE double (IEEE 8-byte)
11:(4,'f'), # FLOAT float (IEEE 4-byte)
10:(8,'ii'), # SRATIONAL (int32, int32)
9:(4,'i'), # SLONG int32
8:(2,'h'), # SSHORT int16
7:(1,'c'), # UNDEFINED
6:(1,'b'), # SBYTE int8
5:(8,'II'), # RATIONAL (uint32, uint32)
4:(4,'I'), # LONG uint32
3:(2,'H'), # SHORT uint16
2:(-1,'c'), # ASCII uint8 (7-bit)
1:(1,'c') # BYTE uint8
}
fieldtype_label = {
0: "UNDEFINED",
1: "BYTE",
2: "ASCII",
3: "SHORT",
4: "LONG",
5: "RATIONAL",
6: "SBYTE",
7: "UNDEFINED",
8: "SSHORT",
9: "SLONG",
10: "SRATIONAL",
11: "FLOAT",
12: "DOUBLE",
}
#
# TIFF Tag dictionary
# Ideally, int maps to tuple of name, and handler/reader or expected data type
#
tagdict = {
254: ("SubfileType"),# 0=Full, 1=Reduced resolution, 2=one page of many, 4=transparency mask
255: ("OldSubfileType"),# 1=full resolution image, 2=reduced image, 3=one page of many
256: ("ImageWidth"),
257: ("ImageLength"),
258: ("BitsPerSample"),
259: ("Compression"),
# 1=None
# 2=CCITT modified Huffman RLE
# 3=CCITT Group 3 fax encoding (T.4)
# 4=CCITT Group 4 fax encoding (T.6)
# 5=LZW
# 6=JPEG
# 7=JPEG DCT
# 9=TIFF/FX T.85 JBIG
# 10=TIFF/FX T.43 Colour by layered JBIG
# 32766=NeXT 2-bit RLE
# 32771=#1 w/ word alignment (CCITRLEW)
# 32773=Macintosh RLE (packbits)
# 32809=Thunderscan RLE
# 32946=Deflate
# 8=Adobe Deflate
# 34712=JPEG2000
# 34925=LZMA2
262: ("PhotometricInterpretation"),
# 0=min value is white
# 1=min value is black
# 2=RGB
# 3=color map indexed (palette)
# 4=mask
# 5=color separation
# 6=YCbCr (CCIR 601)
# 8=CIE L*a*b*
# 9=ICC L*a*b*
263: ("Thresholding"),
# 1=bilevel
# 2=halftone/dithered scan
# 3=floyd-steinberg (error diffuse)
269: ("DocumentName"),
270: ("ImageDescription"),
271: ("Make"),
272: ("Model"),
273: ("StripOffsets"),
274: ("Orientation"),
# 1=top left (normal)
# 2=top right (mirror horizontal)
# 3=bottom right (rotate 180)
# 4=bottom left (mirror vertical)
# 5=left top (row 0 lhs, col 0 top) mirror h rotate 270 CW
# 6=right top (rotate90 cw)
# 7=right bottom mirror h rotate 90 cw
# 8=left bottom rotate 270 cw
277: ("SamplesPerPixel"),
278: ("RowsPerStrip"),
279: ("StripByteCounts"),
280: ("MinSampleValue"),
281: ("MaxSampleValue"),
282: ("XResolution"),
283: ("YResolution"),
284: ("PlanarConfiguration"), # 1=contiguous 2=separate planes
285: ("PageName"),
286: ("XPosition"),
287: ("YPosition"),
288: ("FreeOffsets"),
289: ("FreeByteCounts"),
290: ("GrayResponseUnit"),
291: ("GrayResponseCurve"),
296: ("ResolutionUnit"),
# 1=None
# 2=Inch
# 3=cm (metric)
300: ("ColorResponseUnit"),
301: ("TransferFunction"),
305: ("Software"),
306: ("DateTime"),
315: ("Artist"),
316: ("HostComputer"),
317: ("Predictor"),# 1=None, 2=Horizontal, 3=Floating-point horizontal
318: ("WhitePoint"),
319: ("PrimaryChromaticities"),
320: ("ColorMap"),
322: ("TileWidth"), # tile width in pixels (cols)
323: ("TileLength"),# tile height in pixels (rows)
324: ("TileOffsets"),
325: ("TileByteCounts"),
330: ("SubIFD"),
332: ("InkSet"), # 1=CMYK 2=MultiInk (hi-fi)
333: ("InkNames"),
334: ("NumberOfInks"),
336: ("DotRange"),
337: ("TargetPrinter"),
338: ("ExtraSamples"), #0=Unspecified, 1=Associated alpha, 2=unassociated alpha
339: ("SampleFormat"),
# 1=unsigned int
# 2=signed int
# 3=IEEE FP
# 4=untyped (void)
# 5=complex signed int
# 6=complex IEEE FP
340: ("MinSampleValue"),
341: ("MaxSampleValue"),
346: ("Indexed"), # 0=not indexed, 1=indexed
351: ("OPIProxy"), # 0=no hi-res, 1=hi-res exists
404: ("VersionYear"), #
405: ("ModeNumber"), #
512: ("JPEGProcessingAlg"), # 1=Baseline, 14=Lossless
513: ("JPEGInterchangeFormat"),
514: ("JPEGInterchangeFormatLength"),
529: ("YCbCrCoefficients"),
530: ("YCbCrSubSampling"),
531: ("YCbCrPositioning"),
532: ("ReferenceBlackWhite"),
700: ("XMP"),
28672: ("SonyRawFileType"), #Sony ARW
28673: ("SonyUndocumented"), #Sony ARW
28688: ("SonyCurve"), #Sony ARW
32997: ("ImageDepth"),
32998: ("TileDepth"),
33421: ("CFARepeatPatternDim"),
33422: ("CFAPattern"),
33432: ("Copyright"),
33434: ("ExposureTime"),
33437: ("FNumber"),
33550: ("ModelPixelScaleTag"), #GeoTIFF
33723: ("IPTC"),
33920: ("IntegraphMatrixTag"), #IrasB
33922: ("ModelTiePointTag"), #GeoTIFF
34019: ("RasterPadding"), # 0=Byte,1=Word,2=Long,9=Sector,10=Long Sector
34264: ("ModelTransformationTag"), #GeoTIFF
34665: ("Exif"), # Exif IFD
34675: ("ICCProfile"),
34732: ("ImageLayer"),
34735: ("GeoKeyDirectoryTag"), #GeoTIFF
34736: ("GeoDoubleParamsTag"), #GeoTIFF
34737: ("GeoAsciiParamsTag"), #GeoTIFF
34850: ("ExposureProgram"),
# 0=Undefined
# 1=Manual
# 2=Program AE
# 3=Aperture-priority AE
# 4=Shutter speed priority AE
# 5=Creative(slow speed)
# 6=Action(High speed)
# 7=Portrait
# 8=Landscape
# 9=Bulb
34852: ("SpectralSensitivity"),
34853: ("GPSInfo"),# Sub IFD
34855: ("ISOSpeedRatings"),
34856: ("OptoElectricConversionFactor"),
34857: ("Interlace"),
34858: ("TimeZoneOffset"),
34859: ("SelfTimeMode"),
34864: ("SensitivityType"),
34865: ("StandardOutputSensitivity"),
34866: ("RecommendedExposureIndex"),
34867: ("ISOSpeed"),
34868: ("ISOSpeedLatitudeyyy"),
34869: ("ISOSpeedLatitudezzz"),
36864: ("ExifVersion"),
36867: ("DateTimeOriginal"),
36868: ("DateTimeDigitized"),
37121: ("ComponentsConfiguration"),
37122: ("CompressedBitsPerPixel"),
37377: ("ShutterSpeedValue"),
37378: ("ApertureValue"),
37379: ("BrightnessValue"),
37380: ("ExposureBiasValue"),
37381: ("MaxApertureValue"),
37382: ("SubjectDistance"),
37383: ("MeteringMode"),
37384: ("LightSource"),
#
37385: ("Flash"),
37386: ("FocalLength"),
37396: ("SubjectArea"),
37398: ("TIFF/EPStandardID"),
37399: ("SensingMethod"),
37500: ("MakerNote"),
37510: ("UserComment"),
37520: ("SubsecTime"),
37521: ("SubsecTimeOriginal"),
37522: ("SubsecTimeDigitized"),
40960: ("FlashPixVersion"),
40961: ("ColorSpace"),
40962: ("PixelXDimension"),
40963: ("PixelYDimension"),
40964: ("RelatedSoundFile"),
40965: ("InteropOffset"),
41483: ("FlashEnergy"),
41484: ("SpatialFreqResponse"),
41486: ("FocalPlaneXResolution"),
41487: ("FocalPlaneYResolution"),
41488: ("FocalPlaneResolutionUnit"),
41492: ("SubjectLocation"),
41493: ("ExposureIndex"),
41495: ("SensingMethod"),
41728: ("FileSource"),
41729: ("SceneType"),
41730: ("CFAPattern"),
41985: ("CustomRendered"),
41986: ("ExposureMode"),
41987: ("WhiteBalance"),
41988: ("DigitalZoomRatio"),
41989: ("FocalLengthIn35mmFilm"),
41990: ("SceneCaptureType"),
41991: ("GainControl"),
41992: ("Contrast"),
41993: ("Saturation"),
41994: ("Sharpness"),
41995: ("DeviceSettingDescription"),
41996: ("SubjectDistanceRange"),
42016: ("ImageUniqueID"),
42034: ("LensSpecification"), # minfocal, maxfocal, minFnumMinFocal, minFnumMaxFocal
42035: ("LensMake"),
42036: ("LensModel"),
42037: ("LensSerialNumber"),
42112: ("GDAL_METADATA"),
42113: ("GDAL_NODATA"),
42240: ("Gamma"),
50706: ("DNGVersion"),
50708: ("UniqueCameraModel"),
50709: ("LocalizedCameraModel"),
50710: ("CFAPlaneColor"),
50711: ("CFALayout"),
50341: ("PrintImageMatching"),
50740: ("DNGPrivateData"),
50828: ("OriginalRawFileData"),
50829: ("ActiveArea"),
50936: ("ProfileName"),
50937: ("ProfileHueSatMapDims"),
50938: ("ProfileHueSatMapData1"),
50939: ("ProfileHueSatMapData2"),
50940: ("ProfileToneCurve"),
50941: ("ProfileEmbedPolicy"),
50942: ("ProfileCopyright"),
50964: ("ForwardMatrix1"),
50965: ("ForwardMatrix2"),
50966: ("PreviewApplicationName"),
50967: ("PreviewApplicationVersion"),
50968: ("PreviewSettingsName"),
50969: ("PreviewSettingsDigest"),
50970: ("PreviewColorSpace"),
50971: ("PreviewDateTime"),
50972: ("RawImageDigest"),
}
#
# GPS specific tags on GPSInfo SubIFD
#
gps_tagdict = {
0: ("GPSVersionID"),
1: ("GPSLatitudeRef"),
2: ("GPSLatitude"),
3: ("GPSLongitudeRef"),
4: ("GPSLongitude"),
5: ("GPSAltitudeRef"),
6: ("GPSAltitude"),
7: ("GPSTimeStamp"),
8: ("GPSSatellites"),
9: ("GPSStatus"), # A=Active, V=Void
10: ("GPSMeasureMode"), # 2=2d, 3=3d
11: ("GPSDOP"),
12: ("GPSSpeedRef"), # K=kph, M=mph, N=knots
13: ("GPSSpeed"),
14: ("GPSTrackRef"), # M=Mag North, T=True North
15: ("GPSTrack"),
16: ("GPSImgDirectionRef"), # M=Mag North, T=True North
17: ("GPSImgDirection"),
18: ("GPSMapDatum"),
19: ("GPSDestLatitudeRef"), # N, S
20: ("GPSDestLatitude"),
21: ("GPSDestLongitudeRef"), # E,W
22: ("GPSDestLongitude"),
23: ("GPSDestBearingRef"), #M, T
24: ("GPSDestBearing"),
25: ("GPSDestDistanceRef"), # K,M, N
26: ("GPSDestDistance"),
27: ("GPSProcessingMethod"), # GPS, CELLID, WLAN, MANUAL
28: ("GPSAreaInformation"),
29: ("GPSDateStamp"), #YYYY:mm:dd
30: ("GPSDifferential"), # 1=Differential corrected
31: ("GPSHPositioningError"),
}
makernote_nikon_tagdict = {
1: ("MakerNoteVersion"),# 0210=D60
2: ("ISO"),
3: ("ColorMode"),
4: ("Quality"),
5: ("WhiteBalance"),
6: ("Sharpness"),
7: ("FocusMode"),
8: ("FlashSetting"),
9: ("FlashType"),
11: ("WhiteBalanceFineTune"),
12: ("WB_RBLevels"),
13: ("ProgramShift"),
14: ("ExposureDifference"),
15: ("ISOSelection"),
16: ("DataDump"),
17: ("PreviewIFD"),
18: ("FlashExposureComp"),
19: ("ISOSetting"),
20: ("ColorBalanceA"),
22: ("ImageBoundary"),
23: ("ExternalFlashExposureComp"),
24: ("FlashExposureBracketValue"),
25: ("ExposureBracketValue"),
26: ("ImageProcessing"),
27: ("CropHighSpeed"),
28: ("ExposureTiming"),
29: ("SerialNumber"),
30: ("ColorSpace"),
31: ("VRInfo"),
32: ("ImageAuthentication"),
33: ("FaceDetect"),
34: ("Active D-Lighting"),
35: ("PictureControlData"),
36: ("WorldTime"),
37: ("ISOInfo"),
43: ("DistortInfo"),
131: ("LensType"),
132: ("Lens"),
133: ("ManualFocusDistance"),
134: ("DigitalZoom"),
135: ("FlashMode"),
136: ("AFInfo"),
137: ("ShootingMode"), # bitpos
# :0 = continuous
# :1 = delay
# :2 = PC Control
# :3 = Self-timer
# :4 = Exposure Bracketing
# :5 = Auto ISO
# :6 = WB Bracketing
# :7 = IR Control
# :8 = D-Lighting Bracketing
149: ("NoiseReduction"),
153: ("RawImageCenter"),
154: ("SensorPixelSize"),
156: ("SceneAssist"),
158: ("RetouchHistory"),
160: ("SerialNumber"),
162: ("ImageDataSize"),
165: ("ImageCount"),
166: ("DeletedImageCount"),
167: ("ShutterCount"),
185: ("AFTune"),
187: ("RetouchInfo"),
}
sony_tagdict = {
16: ("CameraInfo"),
32: ("FocusInfo"),
258: ("Quality"),
8206: ("PictureEffect"), #0 single
8207: ("SoftSkinEffect"), #0 single
8241: ("SerialNumber"), #0 single
36875: ("FaceDetectSetting"), #0 single
45056: ("FileFormat"), #0 single
45057: ("SonyModelID"), #0 single
45088: ("CreativeStyle"), #0 single
45089: ("ColorTemperature"), #0 single
45090: ("ColorCompensationFilter"), # neg green, pos magenta
45091: ("SceneMode"), #0 single
45092: ("ZoneMatching"), #0 single
45093: ("DynamicRangeOptimizer"), #0 single
45094: ("ImageStabilization"), #0 single
45095: ("LensType"), #0 single
45097: ("ColorMode"), #0 single
45098: ("LensSpec"), #0 single
45099: ("FullImageSize"), #0 single
45100: ("PreviewImageSize"), #0 single
45129: ("ReleaseMode"), #0 single
45130: ("SequenceNumber"), #0 single
45138: ("IntelligentAuto"),
45140: ("WhiteBalance"),
}
relative_offset = 0
MAX_PRINT = 128
MAX_EXCERPT = 16
skipexif=False
pause = False
# indexed by field type
type_handler = {}
# indexed by tag
tag_handler = {}
# indexed by tag
type_posthandler = {}
def read_offset(f):
return read_int(f)
def subifd_reader(f, ifd):
#raw_input('>')
voffsets = default_reader(f, ifd)
cpos = f.tell()
for i,voffset in enumerate(voffsets):
f.seek(relative_offset+voffset)
print("---SubIFD {} of {}".format(i+1, ifd['count']))
while read_IFD(f):
pass
print("---End SubIFD")
f.seek(cpos)
def nikon_lenstype_posthandler(content):
ctype = ord(content[0])
bincode = "{0:b}".format(ctype)[::-1]
result = []
if bincode[0] == '1': result.append('MF')
if bincode[1] == '1': result.append('D')
if bincode[2] == '1': result.append('G')
if bincode[3] == '1': result.append('VR')
print (" ".join(result))
def makernote_reader(f, ifd):
print (default_reader(f, ifd)[:MAX_PRINT])
#sony_makernote_reader(f, ifd)
def sony_makernote_reader(f, ifd):
voffset = read_offset(f)
current = f.tell()
f.seek(relative_offset+voffset)
#print(read_byte(f, 10))
#print(read_short(f))
read_IFD(f, read_IFD_entry, dict(td=sony_tagdict, tag={}, type=type_handler, post={}))
f.seek(current)
f.close()
exit()
def nikon_makernote_reader(f, ifd):
global endian, relative_offset
old_endian = endian
#print (default_reader(f, ifd)[:MAX_PRINT])
voffset = read_offset(f)
current = f.tell()
f.seek(relative_offset+voffset)
print(read_byte(f, 10))
relative_offset = f.tell()
endian = read_byteorder(f)
print(read_short(f))
print(read_int(f))
raw_input('press <return>')
nikon_posthandler = {
131: nikon_lenstype_posthandler
}
read_IFD(f, read_IFD_entry, dict(td=makernote_nikon_tagdict, tag={}, type=type_handler, post=nikon_posthandler))
f.seek(current)
raw_input('press <return>')
endian = old_endian
relative_offset=0
def exif_reader(f, ifd):
print("EXIF")
voffset = read_offset(f)
cpos = f.tell()
f.seek(voffset)
if not skipexif:
read_IFD(f)
else:
print(voffset)
f.seek(cpos)
def gpsinfo_reader(f, ifd):
print("GPS")
voffset = read_offset(f)
cpos = f.tell()
f.seek(voffset)
if not skipexif:
read_IFD(f, read_IFD_entry, dict(td=gps_tagdict, tag={}, type=type_handler, post={}))
else:
print(voffset)
f.seek(cpos)
def default_reader(f, ifd):
#print(endian+ifd['pycode']*ifd['count'])
is_offset = ifd['bytesize'] > 4 or ifd['bytesize'] <= 0
is_padding = ifd['bytesize']<4
if is_offset:
offset = read_offset(f)
current = f.tell()
f.seek(relative_offset+offset)
if not is_offset and is_padding:
remaining = 4-ifd['bytesize']
try:
content = unpack(endian+ifd['pycode']*ifd['count']+'x'*remaining, f.read(4))
except:
print(remaining, endian+ifd['pycode']+'x'*remaining)
exit(1)
else:
content = unpack(endian+ifd['pycode']*ifd['count'], f.read(ifd['bytesize']))
if is_offset:
f.seek(current)
return content
def read_byte(f, n=1):
if n==1:
return unpack(endian+'c', f.read(1))[0]
else:
return unpack(endian+'c'*n, f.read(n))
def read_short(f, n=1):
if n==1:
return unpack(endian+'H', f.read(2))[0]
else:
return unpack(endian+'H'*n, f.read(2*n))
def read_int(f, n=1):
if n==1:
return unpack(endian+'I', f.read(4))[0]
else:
return unpack(endian+'I'*n, f.read(4*n))
def read_string(f, encoding="ascii"):
tmp = []
scode = 'c'
while True:
try:
cc = unpack(scode, f.read(calcsize(scode)))[0]
if(cc== b'\x00'):
break
except:
break
tmp.append(cc.decode(encoding))
content = repr("".join(tmp))
return content
def string_reader(f, ifd):
# 2
offset = read_offset(f)
current = f.tell()
f.seek(relative_offset+offset)
content = read_string(f)
f.seek(current)
return content
def skip_posthandler(content):
print("SKIPPED")
pass
def photointerp_posthandler(content):
sftype = content[0]
if sftype == 0: type_text = "White is zero"
elif sftype == 1: type_text = "Black is zero"
elif sftype == 2: type_text = "RGB"
elif sftype == 3: type_text = "RGB Palette"
elif sftype == 4: type_text = "Transparency mask"
elif sftype == 5: type_text = "CMYK"
elif sftype == 6: type_text = "YCbCr"
elif sftype == 8: type_text = "CIE L*a*b*"
elif sftype == 9: type_text = "ICC L*a*b*"
elif sftype == 10: type_text = "ITU L*a*b*"
elif sftype == 32803: type_text = "Color Filter Array"
elif sftype == 34892: type_text = "Linear Raw"
else: type_text = "unknown"
print ("{}({})".format(sftype, type_text))
def subfiletype_posthandler(content):
sftype = content[0]
if sftype == 0: type_text = "Full resolution image"
elif sftype == 1: type_text = "Reduced resolution image"
elif sftype == 2: type_text = "one page of multi-page"
elif sftype == 3: type_text = "one page of multi-page reduced"
elif sftype == 4: type_text = "transparency mask"
elif sftype == 5: type_text = "transparency mask of reduced resolution image"
elif sftype == 6: type_text = "transparency mask of multi-page image"
elif sftype == 7: type_text = "transparency mask of multi-page reduced"
else: type_text = "unknown"
print ("{}({})".format(sftype, type_text))
def samplefmt_posthandler(content):
sftype = content[0]
if sftype == 1: type_text = "Unsigned"
elif sftype == 2: type_text = "Signed"
elif sftype == 3: type_text = "Float"
elif sftype == 4: type_text = "Undefined"
elif sftype == 5: type_text = "Complex int"
elif sftype == 6: type_text = "Complex float"
else: type_text = "unknown"
print ("{}({})".format(sftype, type_text))
def compression_posthandler(content):
ctype = content[0]
label = "unknown compression type"
# 1=None
# 2=CCITT modified Huffman RLE
# 3=CCITT Group 3 fax encoding (T.4)
# 4=CCITT Group 4 fax encoding (T.6)
# 5=LZW
# 6=JPEG
# 7=JPEG DCT
# 9=TIFF/FX T.85 JBIG
# 10=TIFF/FX T.43 Colour by layered JBIG
# 32766=NeXT 2-bit RLE
# 32767=Sony ARW
# 32769=Packed RAW
# 32771=#1 w/ word alignment (CCITRLEW)
# 32773=Macintosh RLE (packbits)
# 32809=Thunderscan RLE
# 32946=Deflate
# 8=Adobe Deflate
# 34712=JPEG2000
# 34925=LZMA2
if ctype == 1: label = "Uncompressed"
elif ctype == 5: label = "LZW"
elif ctype in [6,7,99]: label = "JPEG"
elif ctype == 32767: label = "Sony ARW"
elif ctype == 32769: label = "Packed RAW"
elif ctype == 32770: label = "Samsung SRW"
elif ctype == 34713: label = "Nikon NEF"
print("{}({})".format(ctype, label))
def planarconfig_posthandler(content):
ctype = content[0]
if ctype == 0: label = "Contiguous"
elif ctype == 1: label = "Planar"
print("{}({})".format(ctype, label))
def resunit_posthandler(content):
ctype = content[0]
label = "None"
# 1=None
if ctype == 2:
label = "inch"
elif ctype == 3:
label = "centimeter"
print("{}({})".format(ctype, label))
def read_IFD_entry(f, handler):
# Bytes 0-1 The Tag that identifies the field
# Bytes 2-3 The field Type
# field Tag, field Type
tag, vtype = unpack(endian+'HH', f.read(4))
if tag in handler['td']:
#known Tag type
print("{} ({})".format(tag, handler['td'][tag]))
else:
print("{} (Unknown Tag)".format(tag))
# Bytes 4-7 The number of values, 'Count' of the indicated type
vcount = unpack(endian+'i', f.read(4))[0]
print("value count: {}\tType:{}({})".format(vcount, vtype, fieldtype_label[vtype]))
if vtype==0:
print("Unknown data type '{}', skipping".format(vtype))
return
# estimate data size, if datasize > 4-byte then the value contains offset instead of value
tsize,tcode = vtypes[vtype]
datasize = tsize*vcount
ifd_entry_info = dict(tag=tag, type=vtype, count=vcount, pycode=tcode, elemsize=tsize, bytesize=datasize)
# tag require special bytestream reader
if tag in handler['tag']:
return handler['tag'][tag](f, ifd_entry_info)
if vtype in handler['type']:
# bytestream extraction of type requires preprocessing or padding
content = handler['type'][vtype](f, ifd_entry_info)
else:
content = default_reader(f, ifd_entry_info)
if tag in handler['post']:
handler['post'][tag](content)
else:
if(vcount)<MAX_PRINT:
print("value: ", content)
else:
print("excerpt: ", content[:MAX_EXCERPT], '...')
def read_IFD(f, entry_fn=read_IFD_entry, entry_handler=dict(td=tagdict, tag=tag_handler, type=type_handler, post=type_posthandler)):
print("--------------------------BEGIN IFD")
print(f.tell())
# num IFD
num_ifd = unpack(endian+'H', f.read(2))[0]
if num_ifd==0: print("0 IFD entries")
for i in range(num_ifd):
#IFD entries
print("\nIFD #{} of {}\toffset:{}".format(i+1, num_ifd, f.tell()))
entry_fn(f, entry_handler)
if pause:
print()
inp = raw_input("press <return> to continue >")
if inp.lower() in ['q', 'quit', 'exit']:
return False
tmp = unpack(endian+'I', f.read(4))[0]
print("--------------------------END IFD. Next : ", tmp)
if tmp != 0:
f.seek(relative_offset+tmp)
return tmp!=0
def read_byteorder(f):
tmp = "".join([b.decode("utf-8") for b in unpack("cc", f.read(2))])
if tmp=="II":
_endian = "<"
print("little-endian")
else:
_endian = ">"
print("big-endian")
return _endian
def init_handlers():
""" populate default handlers """
#data type-specific reader
type_handler[2] = string_reader
#tag-specific
tag_handler[330] = subifd_reader
tag_handler[34665] = exif_reader
tag_handler[34853] = gpsinfo_reader
tag_handler[34893] = subifd_reader
tag_handler[37500] = makernote_reader
#existing data types, but requires post-processing
type_posthandler[254] = subfiletype_posthandler
type_posthandler[259] = compression_posthandler
type_posthandler[262] = photointerp_posthandler
type_posthandler[284] = planarconfig_posthandler
type_posthandler[296] = resunit_posthandler
type_posthandler[324] = skip_posthandler
type_posthandler[325] = skip_posthandler
type_posthandler[339] = samplefmt_posthandler
#type_posthandler[34735] = geokey_posthandler
if __name__=="__main__":
parser = argparse.ArgumentParser()
parser.add_argument('inputfile', nargs=1)
parser.add_argument('-pause', help='pause on each display of IFD entry', action='store_true', default=False)
parser.add_argument('-skipexif', help='do not read exif sub IFD', action='store_true', default=False)
parser.add_argument('-maxlen', help='maximum length of array element to display', type=int, default=MAX_PRINT)
parser.add_argument('-exlen', help='maximum length of displayed excerpt of array elements', type=int, default=MAX_EXCERPT)
args = parser.parse_args()
if not path.exists(args.inputfile[0]):
exit("file not found")
pause = args.pause
skipexif = args.skipexif
MAX_PRINT = args.maxlen
MAX_EXCERPT = args.exlen
filename = args.inputfile[0]
base, ext = path.splitext(filename)
init_handlers()
with open(filename, "rb") as f:
current = f.tell()
f.seek(0, os.SEEK_END)
fsize = f.tell()
print('file size: ', fsize)
f.seek(current)
#
# Byte 0-1 endianness
#
endian = read_byteorder(f)
#
# Byte 2-3 TIFF Identifier
#
tmp = unpack(endian+'H', f.read(2))[0]
print("TIFF Identifier: 42 = ", tmp)
assert(tmp==42)
# offset of the first IFD, usually 0
tmp = unpack(endian+'I', f.read(4))[0]
print("Offset of 1st IFD : {} bytes\ncur. offset: {}".format(tmp, f.tell()))
if(tmp != 0 and tmp != f.tell() and tmp<fsize):
f.seek(tmp)
while read_IFD(f):
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment