PBExif - Open Code to read Exif Data

Developed or developing a new product in PureBasic? Tell the world about it.
User avatar
thyphoon
Enthusiast
Enthusiast
Posts: 355
Joined: Sat Dec 25, 2004 2:37 pm

PBExif - Open Code to read Exif Data

Post by thyphoon »

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.

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
User avatar
NicTheQuick
Addict
Addict
Posts: 1517
Joined: Sun Jun 22, 2003 7:43 pm
Location: Germany, Saarbrücken
Contact:

Re: PBExif - Open Code to read Exif Data

Post by NicTheQuick »

You've created the same thread twice.
The english grammar is freeware, you can use it freely - But it's not Open Source, i.e. you can not change it or publish it in altered way.
User avatar
thyphoon
Enthusiast
Enthusiast
Posts: 355
Joined: Sat Dec 25, 2004 2:37 pm

Re: PBExif - Open Code to read Exif Data

Post by thyphoon »

NicTheQuick wrote: Sun Aug 18, 2024 2:43 pm You've created the same thread twice.
Yes Thanks !
that's the problem when you try to post something in an area where the network doesn't work😅
Post Reply