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