PBExif - Open Code to read Exif Data
Posted: Sun Aug 18, 2024 2:08 pm
I share with you the fruit of a few days of vacation. I know there are similar codes already, but I wanted to do this on my own and provide an open code.
If you have any suggestions for improvement, please don't hesitate.I tried to comment as much as possible.
Not right away, but I hope to be able to offer modification and saving of exifs data, as well as compatibility with png, Tiff files. and maybe the ability to read/write XMP data.
If you have any suggestions for improvement, please don't hesitate.I tried to comment as much as possible.
Not right away, but I hope to be able to offer modification and saving of exifs data, as well as compatibility with png, Tiff files. and maybe the ability to read/write XMP data.
Code: Select all
; Program: PBExif
; Description: Read Exif Data from JPG
; Version: 0.2
; Author: Thyphoon
; Date: Aout, 2024
; License: PBExif : Free, unrestricted, credit
; appreciated but not required.
; Note: Please share improvement !
;Version 0.1
;- First Version
;Version 0.2 (First Public version)
;- Add XPTag
;- XPTag Smiley compatible
DeclareModule Exif
#Verbose=#False
; Enumeration for endianness types
Enumeration
#LittleEndian ; Used for little-endian byte order (e.g., Intel processors)
#BigEndian ; Used for big-endian byte order (e.g., Motorola processors)
EndEnumeration
; Structure to hold individual EXIF tag information
Structure ExifTag
TagID.u ; Unique identifier for the EXIF tag
SubTagID.u ; Identifier for sub-tags (0 if not applicable)
DataType.w ; Data type of the tag (e.g., byte, ascii, short, long)
NumComponents.l ; Number of components in the tag's value
DataValue.l ; Actual value or offset to the value
StringValue.s ; String representation of the tag's value
EndStructure
; Structure to hold all EXIF data for a file
Structure ExifData
fh.i ; File handle
FilePath.s ; Path to the file
FileEndianness.b ; Byte order of the file (little or big endian) #LittleEndian or #BigEndian
ExifSize.l ; Size of the EXIF data
List Tags.ExifTag() ; List of all EXIF tags found in the file
EndStructure
; Structure for the EXIF tag database
Structure ExifTagDB
name.s ; Human-readable name of the tag
TagId.s ; String representation of the tag ID
EndStructure
; Main structure to hold global data
Structure Main
SystemEndianness.b ; Endianness of the system
List ExifData.ExifData() ; List of EXIF data for multiple files
; Database mappings
Map GetTagID.s() ; Map to get tag ID from name
Map GetName.s() ; Map to get name from tag ID
EndStructure
; Global variable to hold the main structure
Global Main.Main
; Function declarations
Declare.b InitSystemEndianess() ; Initialize system endianness
Declare AddTag(Name.s, TagId.s) ; Add a tag to the database
Declare.i ReadExif(FilePath.s) ; Declaration of the main function to read EXIF data from a file
Declare.b FreeExif(*eh) ; Free memory used by EXIF data
Declare.s GetTagValueByName(*eh.ExifData, Name.s) ; Get tag value by its name
Declare.s GetTagValueById(*eh.ExifData, TagId.u, SubTagId.u = 0);Get tag value of an EXIF tag by its ID and SubID
; Initialize the system endianness
InitSystemEndianess()
; https://exiftool.org/TagNames/EXIF.html
AddTag("ImageWidth","100")
AddTag("ImageHeight","101")
AddTag("DocumentName","10D")
AddTag("Make","10F")
AddTag("Model","110")
AddTag("Orientation","112")
AddTag("Software","131")
AddTag("ModifyDate","132")
AddTag("Artist","13B")
AddTag("ThumbnailOffset","201")
AddTag("ThumbnailLength","202")
AddTag("DateTimeOriginal","9003"); (date/time when original image was taken)
AddTag("CreateDate","9004") ; (called DateTimeDigitized by the EXIF spec.)
AddTag("XPTitle","9C9B")
AddTag("XPComment","9C9C")
AddTag("XPAuthor","9C9D"); (ignored by Windows Explorer if Artist exists)
AddTag("XPKeywords","9C9E")
AddTag("XPSubject","9C9F")
AddTag("GPSLatitudeRef","1-8825")
AddTag("GPSLatitude","2-8825")
AddTag("GPSLongitudeRef","3-8825")
AddTag("GPSLongitude","4-8825")
AddTag("GPSAltitudeRef","5-8825")
AddTag("GPSAltitude","6-8825")
AddTag("GPSTimeStamp","7-8825")
EndDeclareModule
Module Exif
; Détermine l'endianness du système
; Procedure to determine and initialize the system's endianness
Procedure.b InitSystemEndianess()
Define.l test = 1
; Check the first byte of the integer. If it's 1, the system is little-endian
If PeekB(@test) = 1
Main\SystemEndianness = #LittleEndian
If #Verbose = #True
Debug "System is Little-Endian"
EndIf
Else
Main\SystemEndianness = #BigEndian
If #Verbose = #True
Debug "System is Big-Endian"
EndIf
EndIf
; Return the determined endianness
ProcedureReturn Main\SystemEndianness
EndProcedure
; Procedure to add a tag to the EXIF tag database
Procedure AddTag(Name.s, TagId.s)
; Add the tag ID to the name-to-ID map (converting TagId to uppercase)
Main\GetName(Name) = UCase(TagId)
; Add the tag name to the ID-to-name map (converting TagId to uppercase)
Main\GetTagID(UCase(TagId)) = Name
EndProcedure
; Procedure to convert a word (16-bit) value based on endianness
Procedure.w ConvertWord(value.w, DataEndianess.b)
; If the system endianness differs from the data endianness, swap bytes
If Main\SystemEndianness <> DataEndianess
; Swap the lower and upper bytes of the word
ProcedureReturn ((value & $FF) << 8) | ((value & $FF00) >> 8)
Else
; If endianness matches, return the value unchanged
ProcedureReturn value
EndIf
EndProcedure
; Procedure to convert a long (32-bit) value based on endianness
Procedure.l ConvertLong(value.l, DataEndianess.b)
; If the system endianness differs from the data endianness, swap bytes
If Main\SystemEndianness <> DataEndianess
; Swap all four bytes of the long
ProcedureReturn ((value & $FF) << 24) | ((value & $FF00) << 8) | ((value & $FF0000) >> 8) | ((value & $FF000000) >> 24)
Else
; If endianness matches, return the value unchanged
ProcedureReturn value
EndIf
EndProcedure
;-GPS Info
; Function to parse GPS coordinates from EXIF data
Procedure.d ParseGPSCoordinate(StartTiffHeader, Offset)
; Seek to the start of the GPS coordinate data
FileSeek(Main\ExifData()\fh, StartTiffHeader + Offset)
Protected Degrees.d, Minutes.d, Seconds.d
; GPS coordinates are stored as rational numbers (two longs for each component)
; Each component (degrees, minutes, seconds) is read as a fraction: numerator/denominator
Degrees = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness) / ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Minutes = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness) / ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Seconds = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness) / ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
; Convert degrees, minutes, seconds to decimal degrees
; Formula: Decimal Degrees = Degrees + (Minutes / 60) + (Seconds / 3600)
ProcedureReturn Degrees + (Minutes / 60) + (Seconds / 3600)
EndProcedure
; Function to parse GPS timestamp from EXIF data
Procedure.s ParseGPSTimeStamp(StartTiffHeader, Offset)
FileSeek(Main\ExifData()\fh, StartTiffHeader + Offset)
Protected Hours.d, Minutes.d, Seconds.d
; GPS timestamp is stored similarly to coordinates: as rational numbers
Hours = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness) / ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Minutes = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness) / ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Seconds = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness) / ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
; Format the timestamp as HH:MM:SS.sss
ProcedureReturn StrF(Hours, 0) + ":" + StrF(Minutes, 0) + ":" + StrF(Seconds, 3)
EndProcedure
; Procedure to handle various GPS information tags
Procedure GPSInfo(StartTiffHeader)
; Reference: https://exiftool.org/TagNames/GPS.html
Select Main\ExifData()\Tags()\TagID
Case 1 ; GPSLatitudeRef
; This tag is typically stored directly in DataValue
FileSeek(Main\ExifData()\fh, Loc(Main\ExifData()\fh)-4) ; Move back to read DataValue
Main\ExifData()\Tags()\StringValue = ReadString(Main\ExifData()\fh, #PB_Ascii, 1)
; 'N' indicates northern latitude, 'S' indicates southern latitude
Case 2 ; GPSLatitude
; Parse and store the latitude as a decimal degree string
Main\ExifData()\Tags()\StringValue = StrD(ParseGPSCoordinate(StartTiffHeader, Main\ExifData()\Tags()\DataValue))
Case 3 ; GPSLongitudeRef
; Similar to GPSLatitudeRef
FileSeek(Main\ExifData()\fh, Loc(Main\ExifData()\fh)-4)
Main\ExifData()\Tags()\StringValue = ReadString(Main\ExifData()\fh, #PB_Ascii, 1)
; 'E' indicates eastern longitude, 'W' indicates western longitude
Case 4 ; GPSLongitude
; Parse and store the longitude as a decimal degree string
Main\ExifData()\Tags()\StringValue = StrD(ParseGPSCoordinate(StartTiffHeader, Main\ExifData()\Tags()\DataValue))
Case 5 ; GPSAltitudeRef
FileSeek(Main\ExifData()\fh, StartTiffHeader + Main\ExifData()\Tags()\DataValue)
GPSAltitudeRef = ReadByte(Main\ExifData()\fh)
Main\ExifData()\Tags()\StringValue = Str(Main\ExifData()\Tags()\DataValue)
; 0 means above sea level, 1 means below sea level
If GPSAltitudeRef = 0
; Above Sea Level
Else
; Below Sea Level
EndIf
Case 6 ; GPSAltitude
FileSeek(Main\ExifData()\fh, StartTiffHeader + Main\ExifData()\Tags()\DataValue)
; Altitude is stored as a rational number (meters)
Protected Altitude.d = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness) / ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Main\ExifData()\Tags()\StringValue = StrD(Altitude, 2) ; Store with 2 decimal places
Case 7 ; GPSTimeStamp
Main\ExifData()\Tags()\StringValue = ParseGPSTimeStamp(StartTiffHeader, Main\ExifData()\Tags()\DataValue)
Default
Debug "GPS Tag " + Str(Main\ExifData()\Tags()\TagID) + " Not implemented"
EndSelect
EndProcedure
;-XP Tag
; Function to convert a Unicode code point to its UTF-16 representation
Procedure.s UnicodeToUTF16(codePoint.l)
Protected result.s = Space(4) ; Allocate space for up to 2 UTF-16 characters (4 bytes)
Protected *ptr.word = @result ; Pointer to manipulate the result string as 16-bit words
If codePoint <= $FFFF
; For Basic Multilingual Plane (BMP) characters (U+0000 to U+FFFF)
*ptr\w = codePoint ; Directly assign the code point (2 bytes)
ProcedureReturn PeekS(@result, 1, #PB_Unicode) ; Return as a 1-character Unicode string
Else
; For characters outside BMP (U+10000 to U+10FFFF), use surrogate pairs
; Calculate high surrogate: (codePoint - 0x10000) >> 10 + 0xD800
Protected highSurrogate.w = $D800 | ((codePoint - $10000) >> 10)
; Calculate low surrogate: (codePoint - 0x10000) & 0x3FF + 0xDC00
Protected lowSurrogate.w = $DC00 | ((codePoint - $10000) & $3FF)
*ptr\w = highSurrogate ; Write high surrogate
*ptr = *ptr + 2 ; Move pointer to next word
*ptr\w = lowSurrogate ; Write low surrogate
ProcedureReturn PeekS(@result, 2, #PB_Unicode) ; Return as a 2-character Unicode string
EndIf
EndProcedure
; Function to decode XP tags (UTF-16 encoded strings) from EXIF data
Procedure.s DecodeXPTag(value.s)
Protected decoded.s = "" ; String to store the decoded result
Protected parts.s = value ; Input string of comma-separated byte values
Protected i, charCode.l, lowCode.l, highCode.l
; Process input string two bytes at a time
For i = 1 To CountString(parts, ",") Step 2
; Get low and high bytes
lowCode = Val(StringField(parts, i, ","))
highCode = Val(StringField(parts, i+1, ","))
; Convert signed bytes to unsigned (PureBasic uses signed bytes)
If lowCode < 0 : lowCode + 256 : EndIf
If highCode < 0 : highCode + 256 : EndIf
; Combine high and low bytes into a 16-bit character code
charCode = (highCode << 8) | lowCode
If charCode >= $D800 And charCode <= $DBFF ; Check if it's a high surrogate
; Get the next code (low surrogate)
i + 2 ; Move to the next pair of bytes
lowCode = Val(StringField(parts, i, ","))
highCode = Val(StringField(parts, i+1, ","))
If lowCode < 0 : lowCode + 256 : EndIf
If highCode < 0 : highCode + 256 : EndIf
Protected lowSurrogate.l = (highCode << 8) | lowCode
If lowSurrogate >= $DC00 And lowSurrogate <= $DFFF
; Combine surrogate pair into a single Unicode code point
charCode = ((charCode - $D800) << 10) + (lowSurrogate - $DC00) + $10000
decoded + UnicodeToUTF16(charCode) ; Convert and add to result
EndIf
Else
; For non-surrogate characters, directly convert and add to result
decoded + UnicodeToUTF16(charCode)
EndIf
Next
ProcedureReturn decoded
EndProcedure
; Procedure to read and parse EXIF tags from an IFD (Image File Directory)
Procedure.u ReadTag(StartTiffHeader, OffsetFirstIFD, SubTagID.u = 0)
Debug "#### Start From " + Hex(SubTagID)
; Position file pointer at the start of the IFD
FileSeek(Main\ExifData()\fh, StartTiffHeader + OffsetFirstIFD)
; Read number of directory entries (2-byte unsigned integer)
Protected NumEntries.w = ConvertWord(ReadWord(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Debug "Number of IFD entries: " + Str(NumEntries)
; IFD Entry Structure:
; 2 bytes: Tag ID
; 2 bytes: Data Type
; 4 bytes: Number of Components
; 4 bytes: Data Value or Offset to Data
Protected e.l
For e = 0 To NumEntries - 1
; Position at the start of each IFD entry
FileSeek(Main\ExifData()\fh, StartTiffHeader + OffsetFirstIFD + 12 * e + 2)
; Add a new tag to the list
AddElement(Main\ExifData()\Tags())
With Main\ExifData()\Tags()
\SubTagID = SubTagID
\TagID = ConvertWord(ReadWord(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
\DataType = ConvertWord(ReadWord(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
\NumComponents = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
\DataValue = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Debug "TagID:$" + Hex(\TagID, #PB_Word) + " DataType:" + Str(\DataType) +
" NumComponents:" + Str(\NumComponents) + " Value=" + Str(\DataValue)
; Check for SubIFD tags (EXIF or GPS)
If \TagID = $8825 Or \TagID = $8769
If #Verbose
Debug "Detect SubIFD"
EndIf
ReadTag(StartTiffHeader, \DataValue, \TagID)
Else
; Process tag data based on SubTagID and DataType
Select \SubTagID
Case $8825 ; GPS INFO
GPSInfo(StartTiffHeader)
Default
Select \DataType
Case 1 ; BYTE
; Handle BYTE type (8-bit unsigned integer)
If \NumComponents <= 4
\StringValue = Str(\DataValue)
Else
FileSeek(Main\ExifData()\fh, StartTiffHeader + \DataValue)
\StringValue = ""
For i = 1 To \NumComponents
\StringValue + Str(ReadByte(Main\ExifData()\fh)) + ","
Next
\StringValue = Left(\StringValue, Len(\StringValue) - 1)
EndIf
; Special handling for XP tags (Unicode strings)
If \TagID = $9C9B Or \TagID = $9C9C Or \TagID = $9C9D Or \TagID = $9C9E Or \TagID = $9C9F
\StringValue = DecodeXPTag(\StringValue)
EndIf
Case 2 ; ASCII
; Handle ASCII type (null-terminated string)
If \NumComponents <= 4
FileSeek(Main\ExifData()\fh, Loc(Main\ExifData()\fh) - 4)
\StringValue = ReadString(Main\ExifData()\fh, #PB_Ascii, \NumComponents - 1)
Else
FileSeek(Main\ExifData()\fh, StartTiffHeader + \DataValue)
\StringValue = ReadString(Main\ExifData()\fh, #PB_Ascii, \NumComponents - 1)
EndIf
Case 3 ; SHORT
; Handle SHORT type (16-bit unsigned integer)
If \NumComponents <= 2
\StringValue = Str(ConvertWord(\DataValue, Main\ExifData()\FileEndianness))
Else
FileSeek(Main\ExifData()\fh, StartTiffHeader + \DataValue)
\StringValue = ""
For i = 1 To \NumComponents
\StringValue + Str(ConvertWord(ReadWord(Main\ExifData()\fh), Main\ExifData()\FileEndianness)) + ","
Next
\StringValue = Left(\StringValue, Len(\StringValue) - 1)
EndIf
Case 4 ; LONG
; Handle LONG type (32-bit unsigned integer)
If \NumComponents = 1
\StringValue = Str(\DataValue)
Else
FileSeek(Main\ExifData()\fh, StartTiffHeader + \DataValue)
\StringValue = ""
For i = 1 To \NumComponents
\StringValue + Str(ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)) + ","
Next
\StringValue = Left(\StringValue, Len(\StringValue) - 1)
EndIf
Case 5 ; RATIONAL
; Handle RATIONAL type (two LONGs: numerator and denominator)
If \SubTagID = $8825
\StringValue = StrD(ParseGPSCoordinate(StartTiffHeader, \DataValue))
Else
FileSeek(Main\ExifData()\fh, StartTiffHeader + \DataValue)
\StringValue = ""
For i = 1 To \NumComponents
Protected Numerator.l = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Protected Denominator.l = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
\StringValue + Str(Numerator) + "/" + Str(Denominator) + ","
Next
\StringValue = Left(\StringValue, Len(\StringValue) - 1)
EndIf
Case 7 ; UNDEFINED
; Handle UNDEFINED type (8-bit bytes)
If \NumComponents <= 4
\StringValue = Str(\DataValue)
Else
FileSeek(Main\ExifData()\fh, StartTiffHeader + \DataValue)
\StringValue = ReadString(Main\ExifData()\fh, #PB_Ascii, \NumComponents)
EndIf
Case 9 ; SLONG
; Handle SLONG type (32-bit signed integer)
If \NumComponents = 1
\StringValue = Str(\DataValue)
Else
FileSeek(Main\ExifData()\fh, StartTiffHeader + \DataValue)
\StringValue = ""
For i = 1 To \NumComponents
\StringValue + Str(ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)) + ","
Next
\StringValue = Left(\StringValue, Len(\StringValue) - 1)
EndIf
Case 10 ; SRATIONAL
; Handle SRATIONAL type (two SLONGs: numerator and denominator)
FileSeek(Main\ExifData()\fh, StartTiffHeader + \DataValue)
\StringValue = ""
For i = 1 To \NumComponents
Numerator.l = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Denominator.l = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
\StringValue + Str(Numerator) + "/" + Str(Denominator) + ","
Next
\StringValue = Left(\StringValue, Len(\StringValue) - 1)
Default
\StringValue = "Unsupported DataType: " + Str(\DataType)
EndSelect
EndSelect
EndIf
EndWith
Next
; Calculate the position of the next IFD
Protected OffsetToEndIFD = OffsetFirstIFD + 2 + (NumEntries * 12)
; Seek to the end of the current IFD
FileSeek(Main\ExifData()\fh, StartTiffHeader + OffsetToEndIFD)
; Read the offset to the next IFD (0 if this is the last IFD)
Protected OffsetNextIFD = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
Debug "____ Stop From " + Hex(SubTagID)
ProcedureReturn OffsetNextIFD
EndProcedure
Procedure.i ReadExif(FilePath.s)
; Check if file exists and is not empty
If FileSize(FilePath) < 1
Debug "File not found or empty: " + FilePath
ProcedureReturn #False
EndIf
; Add a new entry to the ExifData list
AddElement(Main\ExifData())
Main\ExifData()\FilePath = FilePath
Protected Size.w
; Open the file for reading
Main\ExifData()\fh = ReadFile(#PB_Any, FilePath)
If Main\ExifData()\fh
; Check if it's a JPEG file by reading the SOI marker (Start Of Image)
If ReadAsciiCharacter(Main\ExifData()\fh) = $FF And ReadAsciiCharacter(Main\ExifData()\fh) = $D8
If #Verbose : Debug "Valid JPEG file" : EndIf
; Scan through JPEG segments
While Not Eof(Main\ExifData()\fh)
If ReadAsciiCharacter(Main\ExifData()\fh) = $FF
Protected.a Marker = ReadAsciiCharacter(Main\ExifData()\fh)
Select Marker
Case $E1 ; APP1 marker, which typically contains EXIF data
Size = ReadWord(Main\ExifData()\fh) ; Read segment size
; Check for EXIF identifier
If ReadString(Main\ExifData()\fh, #PB_Ascii, 4) = "Exif" And ReadWord(Main\ExifData()\fh) = 0
If #Verbose : Debug "EXIF data found" : EndIf
Protected StartTiffHeader.q = Loc(Main\ExifData()\fh)
; Determine byte order (little endian or big endian)
Select ReadWord(Main\ExifData()\fh)
Case $4D4D ; 'MM' for Motorola (big endian)
Main\ExifData()\FileEndianness = #BigEndian
If #Verbose : Debug "File uses big-endian byte order" : EndIf
Case $4949 ; 'II' for Intel (little endian)
Main\ExifData()\FileEndianness = #LittleEndian
If #Verbose : Debug "File uses little-endian byte order" : EndIf
Default
Debug "Invalid byte order marker in EXIF data"
CloseFile(Main\ExifData()\fh)
ProcedureReturn #False
EndSelect
; Convert segment size based on endianness
Size = ConvertWord(Size, Main\ExifData()\FileEndianness)
If #Verbose : Debug "EXIF segment size: " + Str(Size) + " bytes" : EndIf
; Verify TIFF header (magic number 42)
If ConvertWord(ReadWord(Main\ExifData()\fh), Main\ExifData()\FileEndianness) <> $002A
Debug "Invalid TIFF header in EXIF data"
End
EndIf
; Read offset to first IFD (Image File Directory)
Protected.l OffsetFirstIFD = ConvertLong(ReadLong(Main\ExifData()\fh), Main\ExifData()\FileEndianness)
If OffsetFirstIFD < 8
Debug "Invalid IFD0 offset: $" + Hex(OffsetFirstIFD, #PB_Long)
End
EndIf
; Read IFD0 (main image metadata)
Protected OffsetNextIFD = ReadTag(StartTiffHeader, OffsetFirstIFD)
; Check for and read IFD1 (thumbnail metadata) if present
If OffsetNextIFD > 0
Debug "Reading IFD1 (thumbnail metadata)"
ReadTag(StartTiffHeader, OffsetNextIFD)
Else
Debug "No IFD1 found or invalid offset: " + Str(OffsetNextIFD)
EndIf
Else
Debug "EXIF identifier not found in APP1 segment"
EndIf
Case $D9 ; EOI marker (End Of Image)
Debug "End of JPEG file reached"
Break
EndSelect
EndIf
Wend
Else
Debug "Not a valid JPEG file"
ProcedureReturn #False
EndIf
; Process parsed EXIF tags
ForEach Main\ExifData()\Tags()
If FindMapElement(Main\GetTagID(), Hex(Main\ExifData()\Tags()\TagID))
Protected value.s
Debug Main\GetTagID() + " = " + Main\ExifData()\Tags()\StringValue
Else
If #Verbose
Debug "Unimplemented tag: " + Hex(Main\ExifData()\Tags()\TagID)
EndIf
EndIf
Next
CloseFile(Main\ExifData()\fh)
Else
Debug "Failed to open file for reading"
ProcedureReturn #False
EndIf
ProcedureReturn Main\ExifData()
EndProcedure
; Procedure to get the value of an EXIF tag by its name
Procedure.s GetTagValueByName(*eh.ExifData, Name.s)
; Check if the tag name exists in our mapping
If FindMapElement(Main\GetName(), Name)
; Parse the tag ID and subtag ID from the mapping
; Tag IDs can be in format "XXXX" or "XXXX-YYYY" where YYYY is the subtag ID
TagId.u = Val("$" + StringField(Main\GetName(), 1, "-"))
SubTagId.u = Val("$" + StringField(Main\GetName(), 2, "-"))
; Iterate through all tags in the EXIF data
ForEach *eh\Tags()
; Check if both tag ID and subtag ID match
If *eh\Tags()\TagID = TagId And *eh\Tags()\SubTagID = SubTagId
; Return the string value of the matching tag
ProcedureReturn Main\ExifData()\Tags()\StringValue
EndIf
Next
; If no matching tag is found, debug output
Debug Name + " No Find This Data"
Debug Hex(TagId)
Debug Hex(SubTagId)
Else
; If the tag name is not in our mapping, it's not implemented
Debug Name + " Not Implemented"
ProcedureReturn ""
EndIf
EndProcedure
; Procedure to get the value of an EXIF tag by its ID and SubID
Procedure.s GetTagValueById(*eh.ExifData, TagId.u, SubTagId.u = 0)
; Iterate through all tags in the EXIF data
ForEach *eh\Tags()
; Check if both tag ID and subtag ID match
If *eh\Tags()\TagID = TagId And *eh\Tags()\SubTagID = SubTagId
; Return the string value of the matching tag
ProcedureReturn *eh\Tags()\StringValue
EndIf
Next
; If no matching tag is found, debug output
Debug "Tag not found: " + Hex(TagId) + "-" + Hex(SubTagId)
ProcedureReturn ""
EndProcedure
; Procedure to free the memory used by an EXIF data structure
Procedure.b FreeExif(*eh)
; Iterate through all EXIF data structures
ForEach Main\ExifData()
; If we find the matching structure
If Main\ExifData() = *eh
; Free the list of tags
FreeList(Main\ExifData()\Tags())
; Close the file if it's still open
If IsFile(Main\ExifData()\fh)
CloseFile(Main\ExifData()\fh)
EndIf
; Remove this EXIF data structure from the list
DeleteElement(Main\ExifData())
Debug "Data cleared"
ProcedureReturn #True
EndIf
Next
; If we didn't find the structure
Debug "Structure not found"
ProcedureReturn #False
EndProcedure
EndModule
; Conditional compilation: Only execute if this is the main file being compiled
CompilerIf #PB_Compiler_IsMainFile
; Initialize a default file path (current file's directory)
Define DefaultPath.s = "C:\Users\413\Pictures\Photos\2016-09-01 Rentrée Scolaire 2016\IMG_20160901_080552.jpg"
; Open a FileRequester to choose an image file
Define SelectedFile.s = OpenFileRequester("Select an image file", DefaultPath, "Image files|*.jpg;*.jpeg|All files|*.*", 0)
If SelectedFile
; User selected a file
*eh = Exif::ReadExif(SelectedFile)
If *eh
Debug "Selected file: " + SelectedFile
Debug "Model = " + Exif::GetTagValueByName(*eh, "Model")
Debug "GPSLatitude = " + Exif::GetTagValueByName(*eh, "GPSLatitude")
Debug "GPSLongitude = " + Exif::GetTagValueById(*eh, 4, $8825)
Exif::FreeExif(*eh)
Else
Debug "Failed to read EXIF data from " + SelectedFile
EndIf
Else
; User cancelled the file selection
Debug "File selection cancelled"
EndIf
CompilerEndIf