Code archives/File Utilities/JPEG reference parser and EXIF reader
This code has been declared by its author to be Public Domain code.
Download source code
| |||||
| This is a follow up to my JPEG reference parser. I finally got stuck in again and tracked down the infinite loop that had plagued it for about a year! It doesn't implement everything, but will print out the most common stuff, and it should be fairly easy to see how to implement the unimplemented tags, in conjunction with the referenced docs. I don't like Code Archives entries that only contain links -- code here should always be runnable if you ask me -- but this has been arranged in a bunch of semi-organised files and may even be too large to post (though I'll hack it all together and give it a go). Download the zip for best results! Two links are provided below -- email me via my profile if any of them ever fail to work! http://www.hi-toro.com/blitz/exifreader/exifreader.zip https://dl.dropbox.com/u/3592022/blitz/exifreader.zip | |||||
' General JPEG/JFIF file information reader with EXIF data extraction...
' Tested on 15,000+ image files, 13,500 of which are named as JPEGs. Skips or recovers from bad
' data found in some of these files, and safely handles files erroneously labelled as JPEGs.
' Some references...
' http://www.exif.org/Exif2-2.PDF
' http://www.media.mit.edu/pia/Research/deepview/exif.html
' http://regex.info/exif.cgi
' http://www.takenet.or.jp/~ryuuji/minisoft/exifread/english/ - Exif Reader, for validation -- also crashed on endless recursion file; we don't now!
' http://opanda.com/en/iexif/ - Another reader for validation
' The first one is useful if you want to add more interpretations in transtag.bmx
' -- search the PDF and transtag.bmx for the tag name you want (eg. XResolution)
' then perform operations on the received 'value' string according to the docs. The
' meanings of EXIF data types are also in the PDF -- important, as Long means Int
' in Blitz, and we can't use unsigned values, though in practice it shouldn't
' matter for files under 2GB in size...
SuperStrict
' Ugly hack to keep track of which file offsets have already been visited. Necessary
' as some EXIF data contains circular references that otherwise cause infinite loops...
Global IFDsVisited:TList
' D E M O . . .
' SINGLE FILE DEMO:
' This is not much use if it doesn't contain EXIF data!
Local img:String
img = "mydog.jpg"
PrintJPEGInfo img
End
' MULTIPLE FILE DEMO:
' Comment out ^^^ End ^^^ for folder (and sub-folders) demo. Change folder$ below to your image folder...
Global ImageCount:Long
Local folder$
folder = "C:\My Pictures\"
ParseFolder folder
Print ""
Print "Tested " + ImageCount + " image files."
Print ""
End
' Demo function...
Function ParseFolder (dir:String)
If Right (dir:String, 1) <> "\" And Right (dir:String, 1) <> "/"
dir:String = dir:String + "/"
EndIf
Local folder:Int = ReadDir (dir:String)
If folder
Repeat
Local entry:String = NextFile (folder)
If entry = "" Then Exit
If entry <> "." And entry <> ".."
Local file:String
Local full:String
If FileType (dir + entry) = FILETYPE_FILE
file = entry
full = dir
If Right (full, 1) <> "\" And Right (full, 1) <> "/"
full = full + "\"
EndIf
full = full + file
ImageCount = ImageCount + 1
'Print ""
'Print "Reading " + full + " (image number " + ImageCount + ")..."
PrintJPEGInfo full
Else
If FileType (dir + entry) = FILETYPE_DIR
file = entry
If file <> "." And file <> ".."
Local ffolder:String = dir
If Right (ffolder, 1) <> "\" And Right (ffolder, 1) <> "/"
ffolder = ffolder + "\"
EndIf
ffolder = ffolder + file
ParseFolder (ffolder)
EndIf
EndIf
EndIf
EndIf
Forever
EndIf
End Function
' Core functions...
' -------------------------------------------------------------------------
' Main EXIF reader...
' -------------------------------------------------------------------------
Function PrintEXIFInfo (jpeg:TStream, tiffsize:Int)
' --------------------------------------------------------------------
' Local EXIF image file directory parser...
' --------------------------------------------------------------------
' This will loop through the Image File Directory entries, loop through the tag directories
' within each, and recursively loop into offset IFDs...
Local params:ParameterBundle = New ParameterBundle
params.jpeg = jpeg
params.tiffstart = StreamPos (params.jpeg)
params.tiffend = (params.tiffstart + tiffsize) - 1
' -----------------------------------------------------------------
' TIFF header...
' -----------------------------------------------------------------
Select ReadShort (params.jpeg)
Case $4D4D ' Motorola byte format ("MM")
ReadShort (params.jpeg)
' If ReadShort (params.jpeg) = $002A Then TabPrint "[EXIF section's TIFF header in MOTOROLA byte format]"
params.endian = ENDIAN_MOTOROLA
Case $4949 ' Intel byte format ("II")
ReadShort (params.jpeg)
' If ReadShort (params.jpeg) = $2A00 Then TabPrint "[EXIF section's TIFF header in INTEL byte format]"
params.endian = ENDIAN_INTEL
Default
' Prolly won't happen...
Print "[EXIF section's TIFF header in UNDEFINED byte format...]"
Notify "Undefined byte format... I'm outta here!", True
End
End Select
params.ifdoffset = ReadInt (params.jpeg)
If params.endian = ENDIAN_INTEL Then params.ifdoffset = SwapEndianInt (params.ifdoffset)
SeekStream params.jpeg, params.tiffstart + params.ifdoffset ' Almost always 8, but I found at least one image that was different!
' Print "IFDOFFSET Seeked to " + StreamPos (params.jpeg)
' -----------------------------------------------------------------
' Begin with Primary Image Data IFD...
' -----------------------------------------------------------------
' Create/clear global list to record IFD offsets visited (prevents infinite loops
' with broken circular-referencing files)...
IFDsVisited = CreateList ()
ParseIFD params
params = Null
End Function
' NB. ParseIFD and ProcessTag recursively call each other! Scary but works fine...
' Support stuff...
' -------------------------------------------------------------------------
' Some utility functions...
' -------------------------------------------------------------------------
' Just for prettifying this demo...
Function TabPrint (info:String, indent:Int = 0)
' Return
Local tabs:String
For Local loop:Int = 1 To indent + 1
tabs = tabs + "~t"
Next
Print tabs + info
End Function
' Ahem... yeah:
Function SwapEndianInt:Int (value:Int)
Local temp:String = Hex (value)
Return Int ("$" + Right (temp, 2) + Mid (temp, 5, 2) + Mid (temp, 3, 2) + Left (temp, 2))
End Function
Function SwapEndianShort:Short (value:Short)
Local temp:String = Hex (value)
Return Short ("$" + Right (temp, 2) + Mid (temp, 5, 2))
End Function
' Info function for basic JPEG data...
Function PrintImageData (jpeg:TStream)
' Bits per pixel...
Local bpp:Int = ReadByte (jpeg)
' Height and width...
Local height:Int = ReadShort (jpeg)
Local width:Int = ReadShort (jpeg)
' Components per pixel (1 for grayscale, 3 for RGB)...
Local components:Int = ReadByte (jpeg)
' Depth/total colours...
Local depth:Int = bpp * components
Local colors:Int = 2 ^ depth
' Print information...
TabPrint "Image details: " + width + " x " + height + " @ " + depth + "-bit (" + Int (2 ^ depth) + " colours)"
End Function
Function TranslateTag:String (tag:Int, value:String, bonusball:String = "")
Function Unspec:String (value:String)
Return "Unspecified or invalid (reserved by EXIF spec): " + value
End Function
Local numdom:String [] ' Numerator/denominator in rational number strings...
Select tag
' -----------------------------------------------------
' IFD0/1 values...
' -----------------------------------------------------
Case $0100
' "ImageWidth"
Return value
Case $0101
' "ImageLength"
Return value
Case $0102
' "BitsPerSample"
Return value + " bits per sample"
Case $0103
' "Compression"
Select Short (value)
Case 1
Return "Uncompressed image data"
Case 6
Return "JPEG-compressed thumbnail image data"
Default
Return Unspec (value)
End Select
Case $0106
' "PhotometricInterpretation"
Select Short (value)
Case 2
Return "Pixels are in RGB format"
Case 6
Return "Pixels are in YCbCr format"
Default
Return Unspec (value)
End Select
Case $010E
' "ImageDescription"
Return value
Case $010F
' "Make"
Return value
Case $0110
' "Model"
Return value
Case $0111
' "StripOffsets"
Return value
Case $0112
' "Orientation"
' NOTE: These may be incorrect! Struggling to work them out from spec descriptions!
' The rotated values might be flipped too... need to test images with these values...
Select value
Case 1
Return "Normal"
Case 2
Return "Horizontally flipped"
Case 3
Return "Horizontally and vertically flipped"
Case 4
Return "Vertically flipped"
Case 5
Return "Rotated... somehow..."
Case 6
Return "Rotated... somehow..."
Case 7
Return "Rotated... somehow..."
Case 8
Return "Rotated... somehow..."
Default
Return Unspec (value)
End Select
Case $0115
' "SamplesPerPixel"
Return value + " samples per pixel"
Case $0116
' "RowsPerStrip"
Return value
Case $0117
' "StripByteCounts"
Return value
' BELOW: Re. XResolution and YResolution. Since we don't know if ResolutionUnit
' has been read yet, if you need this data to be correctly returned, scan the
' whole file for the EXIF data you're interested in, store it, THEN interpret it.
' So, for this information, you would store XResolution, YResolution, and
' ResolutionUnit. Once the file has been fully read, you can then call
' TranslateTag with the string value of XResolution in the 'value' parameter
' and the string value of ResolutionUnit in the 'bonusball' parameter to get
' the correct results.
' (Careful if you try to perform division on 'value' (eg. "72/1") as I've found
' at least one photo with invalid data that results in a divide-by-zero error!)
Case $011A
' "XResolution"
Select bonusball
Case "2"
Return value + " pixels per inch"
Case "3"
Return value + " pixels per cm"
Default
Return value + " pixels per inch (assumed, as per spec)"
End Select
Case $011B
Select bonusball
Case ""
Return value + " pixels per inch (assumed, as per spec)"
Case "2"
Return value + " pixels per inch"
Case "3"
Return value + " pixels per cm"
Default
Return Unspec (value)
End Select
Case $011C
' "PlanarConfiguration"
Select value
Case 1
Return "Pixels are in 'chunky' format"
Case 2
Return "Pixels are in 'planar' format"
Case 3
Return Unspec (value)
End Select
Case $0128
' "ResolutionUnit"
Select value
Case ""
Return "inches (assumed, as per spec)"
Case 2
Return "inches"
Case 3
Return "cm"
Default
Return Unspec (value)
End Select
Case $012D
' "TransferFunction"
Return value
Case $0131
' "Software"
Return value
Case $0132
' "DateTime"
Return value
Case $013B
' "Artist"
Return value
Case $013E
' "WhitePoint"
Return value
Case $013F
' "PrimaryChromaticities"
Return value
Case $0201
' "JPEGInterchangeFormat"
Return "JPEG-compressed thumbnail data stored at byte offset #" + value
Case $0202
' "JPEGInterchangeFormatLength"
If value = 0
Return "Empty JPEG-compressed thumbnail data"
Else
Return "JPEG-compressed thumbnail data stored in " + value + " bytes"
EndIf
Case $0211
' "YCbCrCoefficients"
Return value
Case $0212
' "YCbCrSubSampling"
Return value
Case $0213
' "YCbCrPositioning"
' Probably means something to somebody...
Select value
Case 1
Return "Chrominance components centered in relation to luminance"
Case 1
Return "Chrominance components co-sited with luminance"
End Select
Case $0214
' "ReferenceBlackWhite"
Return value
Case $8298
' "Copyright"
Return value
' -----------------------------------------------------
' EXIF Sub-IFD values...
' -----------------------------------------------------
Case $0000829A
' "ExposureTime"
Return value
Case $0000829D
' "FNumber"
Return value
Case $00008822
' "ExposureProgram"
Select value
Case 0
Return "Not defined"
Case 1
Return "Manual exposure"
Case 2
Return "Normal"
Case 3
Return "Aperture priority"
Case 4
Return "Shutter priority"
Case 5
Return "Creative"
Case 6
Return "Action"
Case 7
Return "Portraits"
Case 8
Return "Landscapes"
Default
Return Unspec (value)
End Select
Case $00008824
' "SpectralSensitivity"
Return value
Case $00008827
' "ISOSpeedRatings"
Return value
Case $00008828
' "OECF"
Return value
Case $00009000
' "ExifVersion"
Local version:String = Int (Left (value, 2)) + "." + Int (Right (value, 2))
' Not widely tested!
While Right (version, 1) = "0"
version = Left (version, Len (version) - 1)
If version = "" Then version = "Unknown"; Exit
Wend
' 0220 becomes 2.2, 0221 becomes 2.21, etc...
Return version
Case $00009003
' "DateTimeOriginal"
Return value
Case $00009004
' "DateTimeDigitized"
Return value
Case $00009101
' "ComponentsConfiguration"
Return value
Case $00009102
' "CompressedBitsPerPixel"
Return value
Case $00009201
' "ShutterSpeedValue"
Return value
Case $00009202
' "ApertureValue"
Return value
Case $00009203
' "BrightnessValue"
Return value
Case $00009204
' "ExposureBiasValue"
Return value
Case $00009205
' "MaxApertureValue"
Return value
Case $00009206
' "SubjectDistance"
Return value
Case $00009207
' "MeteringMode"
Return value
Case $00009208
' "LightSource"
Return value
Case $00009209
' "Flash"
Return value
Case $0000920A
' "FocalLength"
Return value
Case $00009214
' "SubjectArea"
Return value
Case $0000927C
' "MakerNote"
Return value
Case $00009286
' "UserComment"
Return value
Case $00009290
' "SubSecTime"
Return value
Case $00009291
' "SubSecTimeOriginal"
Return value
Case $00009292
' "SubSecTimeDigitized"
Return value
Case $0000A000
' "FlashPixVersion"
Return value
Case $0000A001
' "ColorSpace"
Select value
Case 1
Return "sRGB color space"
Case $FFFF
Return "Uncalibrated color space"
Default
Return Unspec (value)
End Select
Case $0000A002
' "ExifImageWidth"
Return value
Case $0000A003
' "ExifImageHeight"
Return value
Case $0000A004
' "RelatedSoundFile"
Return value
Case $0000A20B
' "FlashEnergy"
Return value
Case $0000A20C
' "SpatialFrequencyResponse"
Return value
Case $0000A20E
' "FocalPlaneXResolution"
Return value
Case $0000A20F
' "FocalPlaneYResolution"
Return value
Case $0000A210
' "FocalPlaneResolutionUnit"
Return value
Case $0000A214
' "SubjectLocation"
Return value
Case $0000A215
' "ExposureIndex"
Return value
Case $0000A217
' "SensingMethod"
Return value
Case $0000A300
' "FileSource"
Return value
Case $0000A301
' "SceneType"
Return value
Case $0000A302
' "CFAPattern"
Return value
Case $0000A401
' "CustomRendered"
Return value
Case $0000A402
' "ExposureMode"
Return value
Case $0000A403
' "WhiteBalance"
Return value
Case $0000A404
' "DigitalZoomRatio"
Return value
Case $0000A405
' "FocalLengthIn35mmFilm"
Return value
Case $0000A406
' "SceneCaptureType"
Return value
Case $0000A407
' "GainControl"
Return value
Case $0000A408
' "Contrast"
Return value
Case $0000A409
' "Saturation"
Return value
Case $0000A40A
' "Sharpness"
Return value
Case $0000A40B
' "DeviceSettingDescription"
Return value
Case $0000A40C
' "SubjectDistanceRange"
Return value
Case $0000A420
' "ImageUniqueID"
Return value
' -----------------------------------------------------
' GPS sub-IFD values...
' -----------------------------------------------------
Case $0
' "GPSVersionID"
Return value
Case $1
' "GPSLatitudeRef"
Return value
Case $2
' "GPSLatitude"
Return value
Case $3
' "GPSLongitudeRef"
Return value
Case $4
' "GPSLongitude"
Return value
Case $5
' "GPSAltitudeRef"
Return value
Case $6
' "GPSAltitude"
Return value
Case $7
' "GPSTimeStamp"
Return value
Case $8
' "GPSSatellites"
Return value
Case $9
' "GPSStatus"
Return value
Case $A
' "GPSMeasureMode"
Return value
Case $B
' "GPSDOP"
Return value
Case $C
' "GPSSpeedRef"
Return value
Case $D
' "GPSSpeed"
Return value
Case $E
' "GPSTrackRef"
Return value
Case $F
' "GPSTrack"
Return value
Case $10
' "GPSImgDirectionRef"
Return value
Case $11
' "GPSImgDirection"
Return value
Case $12
' "GPSMapDatum"
Return value
Case $13
' "GPSDestLatitudeRef"
Return value
Case $14
' "GPSDestLatitude"
Return value
Case $15
' "GPSDestLongitudeRef"
Return value
Case $16
' "GPSDestLongitude"
Return value
Case $17
' "GPSDestBearingRef"
Return value
Case $18
' "GPSDestBearing"
Return value
Case $19
' "GPSDestDistanceRef"
Return value
Case $1A
' "GPSDestDistance"
Return value
Case $1B
' "GPSProcessingMethod"
Return value
Case $1C
' "GPSAreaInformation"
Return value
Case $1D
' "GPSDateStamp"
Return value
Case $1E
' "GPSDifferential"
Return value
End Select
End Function
Function ProcessTag:String (params:ParameterBundle)
' ---------------------------------------------------------
' Reading the tag data...
' ---------------------------------------------------------
' "params.value" contains the value we need to interpret; it
' has already been read and converted to the correct endian-
' ness. Once interpreted, we stick it in a local string
' variable, "finalvalue"...
' Unimplemented cases need to be added as you need them.
' For every data format, datalength is bytespercomponent
' multiplied by params.components. If datalength is 4 or
' less, params.value contains the value required and should
' be interpreted according to the data type. If datalength
' is greater than 4, store the current StreamPos position,
' seek ahead to params.tiffstart + dataoffset, then read
' and interpret the value. Seek back to the previously
' stored position...
Local bytespercomponent:Int
Local datalength:Int
Local formatdesc:String
Local finalvalue:String = ""
' ---------------------------------------------------------
' IMPORTANT! Not all fully implemented!
' ---------------------------------------------------------
' May be some discrepancies relating to unsigned values
' and due to interpreting values differently dependent on
' datalength value (4 or less: use as-is; 5 or more: treat
' as offset to value)...
' ---------------------------------------------------------
' Not all data formats are implemented here, but they appear very
' uncommon. To implement any of these, see the PDF referenced in
' demo.bmx for sizes of types, then follow one of the existing
' examples below...
Select params.dataformat
Case 1
' -----------------------------------------------
' UNSIGNED BYTE
' -----------------------------------------------
formatdesc = formatdesc + "unsigned byte (unimplemented)"
bytespercomponent = 1
datalength = bytespercomponent * params.components
' This CAN happen! See J:\My Pictures/Backdrops/Windows Backdrops/AU-wp1.jpg
'If datalength > 4 Then Notify "unsigned byte not properly implemented!"; End
' Print "datalength " + datalength
Local valueptr:Byte Ptr = Varptr params.value
Case 2
' -----------------------------------------------
' ASCII STRING (should be working)
' -----------------------------------------------
bytespercomponent = 1
datalength = bytespercomponent * params.components
If datalength < 5
Local valueptr:Byte Ptr = Varptr params.value
For Local loop:Int = 0 Until datalength
Local char:Byte = valueptr [loop]
If char
finalvalue = finalvalue + Chr (char)
Else
Exit
EndIf
Next
Else
' It's an offset...
Local dataoffset:Int = params.value
' Go read from file offset, then return here...
Local temp:Int = StreamPos (params.jpeg)
SeekStream params.jpeg, params.tiffstart + dataoffset'; Print "VALUE2 Seeked to " + StreamPos (params.jpeg)
' Can't just use ReadString -- found example with 0-char before end of datalength...
For Local loop:Int = 0 Until datalength
Local char:Byte = ReadByte (params.jpeg)
If char
finalvalue = finalvalue + Chr (char)
Else
Exit
EndIf
Next
SeekStream params.jpeg, temp'; Print "VALUE2ELSE Seeked to " + StreamPos (params.jpeg)
EndIf
Case 3
' -----------------------------------------------
' UNSIGNED SHORT (should be working)
' -----------------------------------------------
bytespercomponent = 2
datalength = bytespercomponent * params.components
If datalength < 5
' finalvalue = Long (Short (params.value))
finalvalue = Short (params.value)
Else
Local dataoffset:Int = params.value
Local temp:Int = StreamPos (params.jpeg)
SeekStream params.jpeg, params.tiffstart + dataoffset'; Print "VALUE3 Seeked to " + StreamPos (params.jpeg)
Local readoff:Short = ReadShort (params.jpeg)
If params.endian = ENDIAN_INTEL
readoff = SwapEndianShort (readoff)
EndIf
finalvalue = Int (readoff)
SeekStream params.jpeg, temp'; Print "VALUE3ELSE Seeked to " + StreamPos (params.jpeg)
EndIf
Case 4
' -----------------------------------------------
' UNSIGNED LONG
' -----------------------------------------------
bytespercomponent = 4
datalength = bytespercomponent * params.components
finalvalue = Int (params.value)
Case 5
' -----------------------------------------------
' UNSIGNED RATIONAL (should be working)
' -----------------------------------------------
bytespercomponent = 8
datalength = bytespercomponent * params.components
If datalength < 5
' Can't happen, two longs = 8 bytes...
' Notify "Oh! It CAN happen... I see. Check unsigned rational conversion with 4 bytes or less!"; End
Else
' It's an offset...
Local dataoffset:Int = params.value
Local temp:Int = StreamPos (params.jpeg)
SeekStream params.jpeg, params.tiffstart + dataoffset'; Print "VALUE5 Seeked to " + StreamPos (params.jpeg)
Local num:Int = ReadInt (params.jpeg)
Local den:Int = ReadInt (params.jpeg)
If params.endian = ENDIAN_INTEL
num = SwapEndianInt (num)
den = SwapEndianInt (den)
EndIf
finalvalue = num + "/" + den
SeekStream params.jpeg, temp'; Print "VALUE5ELSE Seeked to " + StreamPos (params.jpeg)
EndIf
Case 6
' -----------------------------------------------
' SIGNED BYTE
' -----------------------------------------------
formatdesc = formatdesc + "signed byte (unimplemented)"
bytespercomponent = 1
datalength = bytespercomponent * params.components
Case 7
' -----------------------------------------------
' UNDEFINED (case-specific)
' -----------------------------------------------
bytespercomponent = 1
datalength = bytespercomponent * params.components
' Usually implemented as ASCII characters, apparently. Let's wing it and see...
If datalength < 5
Local valueptr:Byte Ptr = Varptr params.value
For Local loop:Int = 0 Until datalength
finalvalue = finalvalue + Chr (valueptr [loop])
Next
Else
' It's an offset...
Local dataoffset:Int = params.value
Local temp:Int = StreamPos (params.jpeg)
SeekStream params.jpeg, params.tiffstart + dataoffset'; Print "VALUE7ELSE Seeked to " + StreamPos (params.jpeg)
For Local loop:Int = 0 Until datalength
finalvalue = finalvalue + Chr (ReadByte (params.jpeg))
Next
SeekStream params.jpeg, temp'; Print "VALUE7ELSE SEEKBACK Seeked to " + StreamPos (params.jpeg)
EndIf
Case 8
' -----------------------------------------------
' SIGNED SHORT
' -----------------------------------------------
formatdesc = formatdesc + "signed short (unimplemented)"
bytespercomponent = 2
datalength = bytespercomponent * params.components
Case 9
' -----------------------------------------------
' SIGNED LONG
' -----------------------------------------------
bytespercomponent = 4
datalength = bytespercomponent * params.components
finalvalue = Int (params.value) ' Signed long = Blitz Int
Case 10
' -----------------------------------------------
' SIGNED RATIONAL (should be working)
' -----------------------------------------------
bytespercomponent = 8
datalength = bytespercomponent * params.components
If datalength < 5
' Can't happen, two longs = 8 bytes...
' Notify "Oh! It CAN happen... I see. Check signed rational conversion with 4 bytes or less!"; End
Else
' It's an offset...
Local dataoffset:Int = params.value
Local temp:Int = StreamPos (params.jpeg)
SeekStream params.jpeg, params.tiffstart + dataoffset'; Print "VALUE10ELSE Seeked to " + StreamPos (params.jpeg)
Local num:Int = ReadInt (params.jpeg)
Local den:Int = ReadInt (params.jpeg)
If params.endian = ENDIAN_INTEL
num = SwapEndianInt (num)
den = SwapEndianInt (den)
EndIf
finalvalue = num + "/" + den
SeekStream params.jpeg, temp'; Print "VALUE10ELSE SEEKBACK Seeked to " + StreamPos (params.jpeg)
EndIf
Case 11
' -----------------------------------------------
' SINGLE FLOAT
' -----------------------------------------------
formatdesc = formatdesc + "single float (unimplemented)"
bytespercomponent = 4
datalength = bytespercomponent * params.components
Case 12
' -----------------------------------------------
' DOUBLE FLOAT
' -----------------------------------------------
formatdesc = formatdesc + "double float (unimplemented)"
bytespercomponent = 8
datalength = bytespercomponent * params.components
Default
' -----------------------------------------------
' UNKNOWN DATA FORMAT!
' -----------------------------------------------
' Ha, this can happen too! J:\My Pictures/Cars/0_2_FullSize~2.jpg and J:\My Pictures/Cars/78vauxhall_equus_21.jpg
formatdesc = formatdesc + "Unknown data format (invalid)"
End Select
Local subifd:Int = False ' Used to skip display of information when sub-IFD tag is found...
Local tagstring:String ' Used in "If Not subifd" section below...
' ----------------------------------------------------------
' OK, what kind of tag is it?
' ----------------------------------------------------------
Select params.tag
' -----------------------------------------------------
' Sub-IFDs: recursively calls ParseIFD on each one...
' -----------------------------------------------------
Case $8769, $8825, $A005
Select params.tag
Case $8769
TabPrint "Found Exif SubIFD:", params.level' at offset " + params.tagpos
Case $8825
TabPrint "Found GPS Info SubIFD:", params.level' at offset " + params.tagpos
Case $A005
TabPrint "Found Interoperability SubIFD:", params.level' at offset " + params.tagpos
End Select
' This is a subifd, not a standard tag...
subifd = True
' -------------------------------------------
' Store current position so we can come back...
' -------------------------------------------
Local temp:Int = StreamPos (params.jpeg)
' Print "At pos BEFORE: " + temp
' Hmmm...
Local offset:Int = params.tiffstart + params.value
Local abort:Int = False
' Check against known-visited "Interoperability SubIFD" file offsets to prevent infinite circular recursion. Ugly...
For Local check:IFDOffset = EachIn IFDsVisited
If check.offset = offset
abort = True
Exit
EndIf
Next
If Not abort ' Only do this if not in visited list!
SeekStream params.jpeg, offset'; Print "VALUE PREPARSE Seeked to " + StreamPos (params.jpeg)
' Interoperability SubIFDs may cause circular references, so add to know-visited list...
If params.tag = $A005
Local off:IFDOffSet = New IFDOffset
off.offset = offset
ListAddLast IFDsVisited, off
EndIf
' ------------------------------------------------
' Go parse sub-IFD...
' ------------------------------------------------
params.level = params.level + 1 ' For TabPrint indenting!
ParseIFD params ' Retro-recursively parse sub-IFD... eek.
params.level = params.level - 1 ' For TabPrint indenting!
' ------------------------------------------------
' Back to where we left off...
' ------------------------------------------------
SeekStream params.jpeg, temp'; Print "VALUE POSTPARSE Seeked to " + StreamPos (params.jpeg)
' Else
' Print "ERROR IN EXIF DATA: Been here before!"
EndIf
' Print "At pos AFTER: " + temp
Default
tagstring = "Unimplemented tag [$" + Hex (params.tag) + "] -- add to ProcessTag function!"
End Select
' ---------------------------------------------------------
' Valid tag, not a sub-IFD, so print information...
' ---------------------------------------------------------
If Not subifd
tagstring = "~q" + TagName (params.tag) + "~q"
'TabPrint "Tag $" + Right (Hex (params.tag), 4) + " (data format " + params.dataformat + ")" + " found at offset " + params.tagpos + ":", params.level
'TabPrint "Tag $" + Right (Hex (params.tag), 4) + ": " + tagstring, params.level
TabPrint "Tag name: " + tagstring, params.level
If finalvalue = ""
finalvalue = params.value + " (uninterpreted integer, meant to be " + formatdesc + ")"
EndIf
' Remove these If/EndIf lines to see Makernote junk (can be binary data hence skipped here)...
If params.tag <> $0000927C
' TabPrint "Value: ~q" + finalvalue + "~q", params.level
TabPrint "Tag value: ~q" + TranslateTag (params.tag, finalvalue) + "~q", params.level
EndIf
TabPrint ""
EndIf
End Function
Function TagName:String (tag:Int)
Select tag
' -----------------------------------------------------
' IFD0/1 values...
' -----------------------------------------------------
Case $0100
Return "ImageWidth"
Case $0101
Return "ImageLength"
Case $0102
Return "BitsPerSample"
Case $0103
Return "Compression"
Case $0106
Return "PhotometricInterpretation"
Case $010E
Return "ImageDescription"
Case $010F
Return "Make"
Case $0110
Return "Model"
Case $0111
Return "StripOffsets"
Case $0112
Return "Orientation"
Case $0115
Return "SamplesPerPixel"
Case $0116
Return "RowsPerStrip"
Case $0117
Return "StripByteCounts"
Case $011A
Return "XResolution"
Case $011B
Return "YResolution"
Case $011C
Return "PlanarConfiguration"
Case $0128
Return "ResolutionUnit"
Case $012D
Return "TransferFunction"
Case $0131
Return "Software"
Case $0132
Return "DateTime"
Case $013B
Return "Artist"
Case $013E
Return "WhitePoint"
Case $013F
Return "PrimaryChromaticities"
Case $0201
Return "JPEGInterchangeFormat"
Case $0202
Return "JPEGInterchangeFormatLength"
Case $0211
Return "YCbCrCoefficients"
Case $0212
Return "YCbCrSubSampling"
Case $0213
Return "YCbCrPositioning"
Case $0214
Return "ReferenceBlackWhite"
Case $8298
Return "Copyright"
' -----------------------------------------------------
' EXIF Sub-IFD values...
' -----------------------------------------------------
Case $0000829A
Return "ExposureTime"
Case $0000829D
Return "FNumber"
Case $00008822
Return "ExposureProgram"
Case $00008824
Return "SpectralSensitivity"
Case $00008827
Return "ISOSpeedRatings"
Case $00008828
Return "OECF"
Case $00009000
Return "ExifVersion"
Case $00009003
Return "DateTimeOriginal"
Case $00009004
Return "DateTimeDigitized"
Case $00009101
Return "ComponentsConfiguration"
Case $00009102
Return "CompressedBitsPerPixel"
Case $00009201
Return "ShutterSpeedValue"
Case $00009202
Return "ApertureValue"
Case $00009203
Return "BrightnessValue"
Case $00009204
Return "ExposureBiasValue"
Case $00009205
Return "MaxApertureValue"
Case $00009206
Return "SubjectDistance"
Case $00009207
Return "MeteringMode"
Case $00009208
Return "LightSource"
Case $00009209
Return "Flash"
Case $0000920A
Return "FocalLength"
Case $00009214
Return "SubjectArea"
Case $0000927C
Return "MakerNote"
Case $00009286
Return "UserComment"
Case $00009290
Return "SubSecTime"
Case $00009291
Return "SubSecTimeOriginal"
Case $00009292
Return "SubSecTimeDigitized"
Case $0000A000
Return "FlashPixVersion"
Case $0000A001
Return "ColorSpace"
Case $0000A002
Return "ExifImageWidth"
Case $0000A003
Return "ExifImageHeight"
Case $0000A004
Return "RelatedSoundFile"
Case $0000A20B
Return "FlashEnergy"
Case $0000A20C
Return "SpatialFrequencyResponse"
Case $0000A20E
Return "FocalPlaneXResolution"
Case $0000A20F
Return "FocalPlaneYResolution"
Case $0000A210
Return "FocalPlaneResolutionUnit"
Case $0000A214
Return "SubjectLocation"
Case $0000A215
Return "ExposureIndex"
Case $0000A217
Return "SensingMethod"
Case $0000A300
Return "FileSource"
Case $0000A301
Return "SceneType"
Case $0000A302
Return "CFAPattern"
Case $0000A401
Return "CustomRendered"
Case $0000A402
Return "ExposureMode"
Case $0000A403
Return "WhiteBalance"
Case $0000A404
Return "DigitalZoomRatio"
Case $0000A405
Return "FocalLengthIn35mmFilm"
Case $0000A406
Return "SceneCaptureType"
Case $0000A407
Return "GainControl"
Case $0000A408
Return "Contrast"
Case $0000A409
Return "Saturation"
Case $0000A40A
Return "Sharpness"
Case $0000A40B
Return "DeviceSettingDescription"
Case $0000A40C
Return "SubjectDistanceRange"
Case $0000A420
Return "ImageUniqueID"
' -----------------------------------------------------
' GPS sub-IFD values...
' -----------------------------------------------------
Case $0
Return "GPSVersionID"
Case $1
Return "GPSLatitudeRef"
Case $2
Return "GPSLatitude"
Case $3
Return "GPSLongitudeRef"
Case $4
Return "GPSLongitude"
Case $5
Return "GPSAltitudeRef"
Case $6
Return "GPSAltitude"
Case $7
Return "GPSTimeStamp"
Case $8
Return "GPSSatellites"
Case $9
Return "GPSStatus"
Case $A
Return "GPSMeasureMode"
Case $B
Return "GPSDOP"
Case $C
Return "GPSSpeedRef"
Case $D
Return "GPSSpeed"
Case $E
Return "GPSTrackRef"
Case $F
Return "GPSTrack"
Case $10
Return "GPSImgDirectionRef"
Case $11
Return "GPSImgDirection"
Case $12
Return "GPSMapDatum"
Case $13
Return "GPSDestLatitudeRef"
Case $14
Return "GPSDestLatitude"
Case $15
Return "GPSDestLongitudeRef"
Case $16
Return "GPSDestLongitude"
Case $17
Return "GPSDestBearingRef"
Case $18
Return "GPSDestBearing"
Case $19
Return "GPSDestDistanceRef"
Case $1A
Return "GPSDestDistance"
Case $1B
Return "GPSProcessingMethod"
Case $1C
Return "GPSAreaInformation"
Case $1D
Return "GPSDateStamp"
Case $1E
Return "GPSDifferential"
End Select
End Function
Function ParseIFD (params:ParameterBundle)
' ---------------------------------------------------------------
' Local function for processing tags...
' ---------------------------------------------------------------
' ---------------------------------------------------------------
' Image File Directory (IFD) parser (header is in TIFF format)...
' ---------------------------------------------------------------
Print ""
Local dirs:Short
Local loop:Int
Local nextifd:Int
Local count:Int = 0
Repeat
count = count + 1
' Print "DIR COUNT: " + count
' ----------------------------------------------------------
' Loop through directories, read next IFD offset, abort if 0
' ----------------------------------------------------------
TabPrint "--------------------------------------------------------------------------------", params.level
TabPrint "Reading new IFD", params.level
TabPrint "--------------------------------------------------------------------------------", params.level
'Print ""
'Print "DIRS START at " + StreamPos (params.jpeg)
dirs = ReadShort (params.jpeg)
If params.endian = ENDIAN_INTEL Then dirs = SwapEndianShort (dirs)
' Print "Reading " + dirs + " directories..."
Local abort:Int = False
' ----------------------------------------------------------
' Directory loop...
' ----------------------------------------------------------
For loop = 0 Until dirs
' Loop #7 recursing...
' -----------------------------------------------------
' 12 bytes per entry...
' -----------------------------------------------------
params.tagpos = StreamPos (params.jpeg)
params.tag = ReadShort (params.jpeg)
params.dataformat = ReadShort (params.jpeg)
params.components = ReadInt (params.jpeg)
params.value = ReadInt (params.jpeg)
If params.endian = ENDIAN_INTEL
params.tag = SwapEndianShort (params.tag)
params.dataformat = SwapEndianShort (params.dataformat)
params.components = SwapEndianInt (params.components)
params.value = SwapEndianInt (params.value)
EndIf
' Print "TAG AT " + params.tagpos + ": $" + Hex (params.tag)
' Invalid tag values in J:\My Pictures\Cars\78vauxhall_equus_21.jpg ! Offset 268...
' Also get this in J:\My Pictures/Music/Pixies/buenos aires 05-02-03.jpg after Interoperability IFD tag...
If params.dataformat = 0 Then Print "INVALID DATA FORMAT ON LOOP " + loop; abort = True; Exit
ProcessTag params
Next
' Print "DIRS STOP at " + StreamPos (params.jpeg)
' ----------------------------------------------------------
' Assume more IFDs to read if valid data format was found...
' ----------------------------------------------------------
If Not abort
nextifd = ReadInt (params.jpeg) ' THIS IS IT... probably.
If params.endian = ENDIAN_INTEL Then nextifd = SwapEndianInt (nextifd)
' Seek to next IFD...
If nextifd
Local seekto:Int = params.tiffstart + nextifd
' Make sure where we're going is within the TIFF section of
' the JPEG file... or we'll crash/loop forever on broken files...
If seekto < params.tiffstart Or seekto > params.tiffend
nextifd = 0 ' Invalid TIFF pointer!
Else
SeekStream params.jpeg, seekto'; Print "NEXTIFD Seeked to " + StreamPos (params.jpeg)
EndIf
EndIf
Else
nextifd = 0 ' Abort, bad data format, assume no more IFDs...
EndIf
Until nextifd = 0
TabPrint "--------------------------------------------------------------------------------", params.level
TabPrint "", params.level
End Function
Function PrintJPEGInfo (f:String, singleframe:Int = False)
Print ""
Print "Info for " + f
Local jpeg:TStream = BigEndianStream (ReadFile (f))
If jpeg
Try
' Start of Image (SOI) marker ($FFD8) -- MUST BE PRESENT!
If ReadByte (jpeg) = $FF And ReadByte (jpeg) = $D8
' TabPrint ""
' TabPrint "--------------------------------------------------------------------------------"
' TabPrint "Start of Image marker $D8 found at byte offset 0"
' TabPrint "--------------------------------------------------------------------------------"
' TabPrint "Assuming JPEG file"
Local loop:Int ' For byte seek loops...
Local datalength:Int ' Block length store
Local checkff:Byte ' Byte to be tested for $FF (start of block)...
Local marker:Byte ' Block marker...
Local startofblock:Int
Local startofdata:Int
Local blockbytesread:Int
Local markerinfo:String
Local scandata:Int
Local printinfo:Int
Local exifcount:Int
Local alldone:Int
While Not Eof (jpeg)
' Searching for blocks beginning with $FF, then single byte marker, then data...
' |FFxx|length_of_block|data_data_data...
' |FFxx|length_of_block| is four bytes total, two each...
' ---------------------------------------------------------
' You are here --> |FFxx|length_of_block|data_data_data...
' ---------------------------------------------------------
startofblock = StreamPos (jpeg)
' Looking for FF first...
Repeat
checkff = ReadByte (jpeg) ' Some Photoshop 7 files have a huge string of zeroes directly after block's stated data length
Until checkff
' Used later...
startofdata = 0
blockbytesread = 0
datalength = 0
markerinfo = ""
If checkff = $FF
' ... then xx, the byte AFTER the FF block marker, skipping if FF (padding)...
Repeat
marker = ReadByte (jpeg)
Until marker <> $FF
' -----------------------------------------------------
' We are now here --> |length_of_block|data_data_data...
' -----------------------------------------------------
' Grab next two bytes (length of block) before proceeding, unless marker is standalone...
Select marker
Case $D0, $D1, $D2, $D3, $D4, $D5, $D6, $D7, $D8, $D9, $0, $FF
' Standalone markers with no following data.
Default
datalength = ReadShort (jpeg) - 2 ' The 2 subtracted bytes store the length itself...
End Select
' -----------------------------------------------------
' Now here --> |data_data_data...
' -----------------------------------------------------
' Record start of data so we can deduce how many bytes are read in each Case afterwards...
startofdata = StreamPos (jpeg)
scandata = False
printinfo = False
Select marker
Case $0, $FF
' Ignore these...
Case $C0
markerinfo = "Start of Frame (Huffman Baseline DCT)"
printinfo = True
Case $C1
markerinfo = "Start of Frame (Huffman Extended Sequential DCT)"
printinfo = True
Case $C2
markerinfo = "Start of Frame (Huffman Progressive DCT)"
printinfo = True
Case $C3
markerinfo = "Start of Frame (Huffman Lossless Seqential)"
printinfo = True
Case $C4
markerinfo = "Define Huffman Table"
Case $C5
markerinfo = "Start of Frame (Huffman Differential Sequential DCT)"
printinfo = True
Case $C6
markerinfo = "Start of Frame (Huffman Differential Progressive DCT)"
printinfo = True
Case $C7
markerinfo = "Start of Frame (Huffman Differential Lossless Sequential)"
printinfo = True
Case $C8
markerinfo = "Start of Frame (Arithmetic, reserved for JPEG extensions)"
printinfo = True
Case $C9
markerinfo = "Start of Frame (Arithmetic Extended Sequential DCT)"
printinfo = True
Case $CA
markerinfo = "Start of Frame (Arithmetic Progressive DCT)"
printinfo = True
Case $CB
markerinfo = "Start of Frame (Arithmetic Lossless Sequential)"
printinfo = True
Case $CC
markerinfo = "Define Arithmetic Table"
Case $CD
markerinfo = "Start of Frame (Arithmetic Differential Sequential DCT)"
printinfo = True
Case $CE
markerinfo = "Start of Frame (Arithmetic Differential Progressive DCT)"
printinfo = True
Case $CF
markerinfo = "Start of Frame (Arithmetic Differential Lossless Sequential)"
printinfo = True
Case $D0, $D1, $D2, $D3, $D4, $D5, $D6, $D7
markerinfo = "Restart"
' Standalone marker, no following data...
Case $D8
markerinfo = "Start of Image"
' Now going through scan (picture) data and ignoring it because it's bloody complicated...
Local newff:Byte
Local breakout:Int = False
Local startdatascan:Int = StreamPos (jpeg)
Repeat
' Got a $FF value?
If ReadByte (jpeg) = $FF
' See if it's a new block marker...
newff = ReadByte (jpeg)
If (newff <> 0) And (newff <> $FF) ' Ignore 0/FF (possible valid/padding data)...
breakout = True
EndIf
EndIf
Until breakout
' Special case... $D8 can appear multiple times per JPEG file, not just at the end. (Multiple images.)
If newff = $D9
If Eof (jpeg)
alldone = True
EndIf
EndIf
Local scanlength:Int = StreamPos (jpeg) - startdatascan
' Add scan data to datalength...
datalength = (datalength + scanlength) - 2 ' We need to go back two previously read bytes so they can be parsed...
Case $D9
'Print "D9"
markerinfo = "End of Image"
'TabPrint "EOI"
' Now going through scan (picture) data and ignoring it because it's bloody complicated...
Local newff:Byte
Local breakout:Int = False
Local startdatascan:Int = StreamPos (jpeg)
Repeat
'TabPrint "SP: " + StreamPos (jpeg)
' Got a $FF value?
If Eof (jpeg) Then breakout = True'; Print "EOF!"
If ReadByte (jpeg) = $FF
' See if it's a new block marker...
newff = ReadByte (jpeg)
'Print Hex (newff)
' Value after $FF marker is 0 or $FF, both valid padding data, so keep reading,
' otherwise, break out of loop:
If (newff <> 0) And (newff <> $FF) ' Ignore 0/FF (possible valid/padding data)...
breakout = True
EndIf
EndIf
Until breakout
' Special case... $D9 can appear multiple times per JPEG file, not just at the end. (Multiple images.)
If newff = $D9
'Print StreamPos (jpeg)
If Eof (jpeg)
'Print "ALLDONE"'; End
alldone = True
EndIf
EndIf
Local scanlength:Int = StreamPos (jpeg) - startdatascan
' Add scan data to datalength...
datalength = (datalength + scanlength) - 2 ' We need to go back two previously read bytes so they can be parsed...
' Standalone marker, no following data...
Case $DA
markerinfo = "Start of Scan"
'TabPrint "START OF SCAN DATA"
For loop = 0 Until ReadByte (jpeg) ' Components
ReadByte jpeg ' Component ID
ReadByte jpeg ' Huffman table (bits 0-3: AC table; bits 4-7: DC table)
Next
' Ignore these three bytes...
ReadByte (jpeg)
ReadByte (jpeg)
ReadByte (jpeg)
' Now going through scan (picture) data and ignoring it because it's bloody complicated...
Local newff:Byte
Local breakout:Int = False
Local startdatascan:Int = StreamPos (jpeg)
Repeat
' Got a $FF value?
If ReadByte (jpeg) = $FF
' See if it's a new block marker...
newff = ReadByte (jpeg)
Select newff
' Ignore 0 (means valid FF value in scan data), FF (possible padding data), D0-D7 (restart markers)...
Case $0, $FF, $D0, $D1, $D2, $D3, $D4, $D5, $D6, $D7
' Ignore these and move on...
' Anything else? Must be a valid block marker, in theory...
Case $D9
'TabPrint "D9"
breakout = True
Default
breakout = True
End Select
EndIf
Until breakout
' Special case... $D9 can appear multiple times per JPEG file, not just at end! (Multiple images.)
If newff = $D9
'TabPrint "D9 END"
If Eof (jpeg)
alldone = True
EndIf
EndIf
'Print "EOS " + StreamPos (jpeg)
Local scanlength:Int = StreamPos (jpeg) - startdatascan
' Consider scan data to be part of datalength...
datalength = (datalength + scanlength) - 2 ' We need to go back two previously read bytes so they can be parsed...
Case $DB
markerinfo = "Define Quantization Table"
Case $DC
markerinfo = "Define Number of Lines"
Case $DD
markerinfo = "Define Restart Interval"
Case $DE
markerinfo = "Define Hierarchical Progression"
Case $DF
markerinfo = "Expand reference components"
Case $E0 ' JFIF marker
markerinfo = "JFIF/APP0"
' Check for JFIF identification string (generally treated as optional)...
ReadString jpeg, 5
' Or...
' If ReadString (jpeg, 5) = "JFIF" + Chr (0)
'TabPrint "Valid JFIF file"
' Else
'TabPrint "Not a valid JFIF file"
' EndIf
Case $E1 ' EXIF information
markerinfo = "EXIF"
' Only parse if Exif00 is here. Apparently other apps can insert data using
' this tag, which will cause reader to fail if it's not EXIF data...
' TabPrint "EXIF data starts at byte offset " + startofdata + " and is " + datalength + " bytes long"
If ReadString (jpeg, 6) = "Exif" + Chr (0) + Chr (0)
PrintEXIFInfo jpeg, datalength - 6 ' Length of TIFF data containing EXIF information!
exifcount = exifcount + 1
EndIf
' Exit ' Early-out if we only need first lot of EXIF data...
Case $E2, $E3, $E4, $E5, $E6, $E7, $E8, $E9, $EA, $EB, $EC, $EE, $EF
markerinfo = "Unimplemented, application-specific"
Case $ED
markerinfo = "Photoshop/APP14"
Case $FE
markerinfo = "Comment"
Default
markerinfo = "UNIMPLEMENTED"
End Select
' TabPrint ""
' TabPrint "--------------------------------------------------------------------------------"
' TabPrint markerinfo + " marker $" + Right (Hex (marker), 2) + " found at byte offset " + startofblock
' TabPrint "--------------------------------------------------------------------------------"
' If datalength
' TabPrint "Data starts at byte offset " + startofdata + " and is " + datalength + " bytes long"
' Else
' TabPrint "No data for this type of marker"
' EndIf
' If printinfo
'PrintImageData jpeg ' Got a $Cx marker so print info!
' Exit ' Only wanted information for main image, don't need EXIF? Just uncomment this!
' EndIf
Else
' Invalid marker offset, so skip rest of file for safety...
'TabPrint "BOOM! INVALID MARKER: " + Hex (checkff) + " -- skipping rest of file!"
Exit
' End
EndIf
If alldone
'TabPrint ""
'TabPrint "--------------------------------------------------------------------------------"
'TabPrint "End of Image marker $D9 found at end of file -- all done!"
'TabPrint "--------------------------------------------------------------------------------"
'TabPrint ""
Else
' Number of bytes already read in this block...
blockbytesread = StreamPos (jpeg) - startofdata
' Go to next block...
SeekStream jpeg, StreamPos (jpeg) + (datalength - blockbytesread)
'Print "NEXTBLOCK Seeked to " + StreamPos (jpeg)
EndIf
Wend
If exifcount = 0 Then Print "No EXIF data in file."
EndIf
Catch ReadFail:Object
Print "Read error in " + f + "; " + StreamPos (jpeg)
End Try
CloseFile jpeg
Else
TabPrint "File not found!"
EndIf
End Function
' Byte-endianness...
Const ENDIAN_MOTOROLA:Int = 0
Const ENDIAN_INTEL:Int = 1
' --------------------------------------------------------------------
' Bundle of frequently-passed parameters among these functions...
' --------------------------------------------------------------------
Type ParameterBundle
' JPEG's EXIF data is actually stored in an embedded TIFF file...
Field jpeg:TStream ' File
Field tiffstart:Int ' TIFF 'file' start
Field tiffend:Int ' Last byte of TIFF 'file'
Field ifdoffset:Int ' Next IFD offset
Field tag:Short ' Data tag
Field value:Int ' Tag value
Field endian:Int ' Byte order
Field components:Int ' Data components
Field dataformat:Short ' Data format
Field tagpos:Int ' Tag position in file
Field level:Int ' For TabPrint -- indents based on level of recursion
End Type
' Gah, where's Monkey's BoxInt when you need it?
Type IFDOffset
Field offset:Int
End Type |
Comments
None.
Code Archives Forum