Created
July 20, 2018 12:55
-
-
Save pebbie/5b3add920902af7fbfbb4cc6c42eb7ef to your computer and use it in GitHub Desktop.
hopefully generic TIFF file format extraction tool (GeoTIFF, NEF, ARW)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
\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