Reading Exif Data without foreign libraries or programs

Share your advanced PureBasic knowledge/code with the community.
Axolotl
Addict
Addict
Posts: 897
Joined: Wed Dec 31, 2008 3:36 pm

Reading Exif Data without foreign libraries or programs

Post by Axolotl »

Hey folks,

after i had dealt with the exiftool, i wanted to approach the file format a little.
My goal was to find all saved times inside the jpeg file.
So here are my experiments on the topic: "Reading Exif Data without foreign libraries or programs."

I know that (apparently) no one really needs this.
But I had fun fiddling out the specific details and didn't want to let the result gather dust on my hard drive.

And the fun fact: I used the module featue for the first time. didn't hurt :)

Happy coding and stay healthy.

Code: Select all

;/=====================================================================================================================
;| File     : ExifDataTestApp.pb 
;| Purpose  : Show the DateTime Information stored in the Exif data field of Jpeg Files. 
;|            Read information is available only. 
;| 
;|            Specification has so much more information to share... 
;| 
;| Version  : 0.02 
;| 
;| State    : Experimental, tested on only a few jpg images 
;| 
;| OS       : Tested on Windows x64 with ASM Backend only 
;| 
;| License  : MIT 
;| 
;| Copyright (c) 2022 by A.H. (Axolotl) 
;| 
;| ChangeLog : 
;|  0.01  .. first attempt (published on forum) 
;|           Link: 
;| 
;|  0.02  .. added new TAG ImageDescription  
;|           adapted main window with improved Preview for long values 
;| 
;| 
;\=====================================================================================================================

EnableExplicit 
; DebugLevel 9  ; show all debug messages 


; ---== MainWindow ==--------------------------------------------------------------------------------------------------

#ProgramName$          = "ExifDataTestApp" 
#ProgramVersion$       = "0.02" ; internally used + #PB_Editor_BuildCount + "." + #PB_Editor_CompileCount 

#MainCaption$          = "Image Exif Data and more...  V" + #ProgramVersion$ + " ~ EXPERIMENTAL "


Enumeration EWindow 1  ; -----------------------------------------------------------------------------
  #WINDOW_Main 
EndEnumeration 

Enumeration EGadget 1  ; -----------------------------------------------------------------------------
  #GADGET_ExpImageFiles 
  #GADGET_LstImageInfo 
  #GADGET_CnvPreView 
  #GADGET_EdtPreView    ; show selected items (especially very long values) 
EndEnumeration 

Enumeration EImage 1 ; -----------------------------------------------------------------------------
  #IMAGE_PreView 
EndEnumeration 


; -----------------------------------------------------------------------------
;  The example app shows the image, too. 
; -----------------------------------------------------------------------------

UseJPEGImageDecoder() 


; ---== Exif Imaage Data ==--------------------------------------------------------------------------------------------

DeclareModule ExifData  ; <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

  ; ---------------------------------------------------------------------------
  ; Constants 
  ; 
  Enumeration EExifTAG ; Number of the TAG (Exif, TIFF, etc.)  
    #ExifTAG_ImageWidth                = $0100 ; recommended not to use, better use jpeg  
    #ExifTAG_ImageHeight               = $0101 ; recommended not to use, better use jpeg   
    #ExifTAG_BitsPerSample             = $0102 ; 
    #ExifTAG_Compression               = $0103 ; thumbnail stuff .. 
; 
    #ExifTAG_Orientation               = $0112 ; 
    #ExifTAG_XResolution               = $011A ; 
    #ExifTAG_YResolution               = $011B ; 
    #ExifTAG_ResolutionUnit            = $0128 ; 
; ; 
    ; new 
    #ExifTAG_ImageDescription          = $010E ; char string giving the title of the image (no two-char codes) 
; ; 
    #ExifTAG_Make                      = $010F ; "Make", #Exif_Type_ASCII, -1, 0) 
    #ExifTAG_Model                     = $0110 ; "Model", #Exif_Type_ASCII, -1, 0) 
    #ExifTAG_Software                  = $0131 ; "Software", #Exif_Type_ASCII, -1, 0) 
    #ExifTAG_DateTime                  = $0132 ; "DateTime", #Exif_Type_ASCII, 20, 0) 
    #ExifTAG_Artist                    = $013B ; name of the camera owner, photographer or image creator 
    #ExifTAG_Copyright                 = $8298 ; indicate both the photographer and editor copyrights 
; 
    #ExifTAG_ExifVersion               = $9000 ; 36864 (9000.H) | UNDEFINED |   4   | "0232"  ; no NULL termination 
    #ExifTAG_ExifFlashpixVersion       = $A000 ; 40960 (A000.H) | UNDEFINED |   4   | "0100"  ; Flashpix Format Version 1.0 
; 
    #ExifTAG_DateTimeOriginal          = $9003 ; 36867 (9003.H) | ASCII |   20  | None 
    #ExifTAG_DateTimeDigitized         = $9004 ; 36868 (9004.H) | ASCII |   20  | None 
    #ExifTAG_OffsetTime                = $9010 ; 36880 (9010.H) | ASCII |    7  | None       ; including NULL 
    #ExifTAG_OffsetTimeOriginal        = $9011 ; 36881 (9011.H) | ASCII |    7  | None       ; including NULL 
    #ExifTAG_OffsetTimeDigitized       = $9012 ; 36882 (9012.H) | ASCII |    7  | None       ; including NULL 
; 
    #ExifTAG_SubsecTime                = $9290 ; Fractions of seconds for DateTime 
    #ExifTAG_SubsecTimeOriginal        = $9291 ; Fractions of seconds for DateTimeOriginal 
    #ExifTAG_SubsecTimeDigitized       = $9292 ; Fractions of seconds for DateTimeDigitized 
; 
    #ExifTAG_ColorSpace                = $A001 ; Color space information tag 
    #ExifTAG_PixelXDimension           = $A002 ; Valid Image Width  | PixelXDimension | 40962 ~ A002.H ~ SHORT or LONG ~ 1 
    #ExifTAG_PixelYDimension           = $A003 ; Valid Image Height | PixelYDimension | 40963 ~ A003.H ~ SHORT or LONG ~ 1 
  EndEnumeration ; EExifTAG 

  ; ---------------------------------------------------------------------------

  Declare.i ReadExifDataFromFile(FileName$)  ; 
  Declare FreeExifData()  ; 

  Declare ShowResultsOnGadget(Gadget) 

  Declare.i GetExifTagAsInteger(ExifTag, DefaultValue = -1) 
  Declare.s GetExifTagAsString(ExifTag, DefaultValue$ = "") 

EndDeclareModule 


Module ExifData  ; <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

  EnableExplicit 

  ; ---------------------------------------------------------------------------
  ; Constants 
  ; 
  Enumeration EExifTAG ; Number of the TAG (Exif, TIFF, etc.)  
    #ExifTAG_Unsupported               = $0000 ; internal define .. 
    #ExifTAG_ExifIFD                   = $8769 ;                |  LONG     |   1   | 
  EndEnumeration ; EExifTAG 
  ; 
  ; HINT: This constant is representing the number of defined äExifTAG_Xxxx in Enumeration EExifTAG 
  ;       If you add new constants to the enumeration (local or global) you must increase the #ExifTagTableSize as well 
  ; 
  #ExifTagTableSize  = 29                      :Debug "HINT: #ExifTagTableSize: " + #ExifTagTableSize 


  ; ---------------------------------------------------------------------------

  Enumeration EJpegMarkers 
    #JM_Start  = $FF  
    ; ... 

    #JM_SOI    = $D8   ; 
    ; ... 

    #JM_APP1   = $E1   ; EXIF and XMP (XMP not supported yet) 
    ; ... 
    #JM_APP13  = $ED   ; IPTC (not supported yet) 
    #JM_APP14  = $EE   ; (not supported yet) 
    #JM_APP15  = $EF   ; (not supported yet) 
    ; ... 
    #JM_JPG0   = $F0   ; JPG0 == 0xF0 to JPG13 == 0xFD 
    #JM_COM    = $FE   
  EndEnumeration ; EJpegMarkers 

  Enumeration EExifByteOrderMark 
    #ExifByteOderMark_Intel    = $4949         ; Little-endian | 0x4D - 0x49 == 0x04 | II 
    #ExifByteOderMark_Motorola = $4D4D         ; Big-endian    |                     | MM 
  EndEnumeration ; EExifByteOrder 

  Enumeration EExifByteOrder 
    #ExifByteOrder_Motorola 
    #ExifByteOrder_Intel 
  EndEnumeration ; EExifByteOrder 

  ; ---------------------------------------------------------------------------

  #TIFF_TAG_Mark               = $002A         ; constant in correct byte order 
  #TIFF_FirstIFDOffset         = $00000008     ; default offset to the first IDD 

  ; ---------------------------------------------------------------------------
  ; Structure User Defined Types 
  ; 
  Structure TByteArray ; Access to the Image Memory byte by byte 
    Byte.a[0] 
  EndStructure 

  ; ---------------------------------------------------------------------------

  Structure TExifTagEntry  ; TAG structure for ExifTags, etc. 
    Number.i           ; 
    Name$              ; 
   ;Descr$             ; .. Description, <sorry, to much typing or formatting)  
    Private.i          ; .. #False or #True (usage makes sense only inside module) 
  EndStructure 

  ; ---------------------------------------------------------------------------

  Structure TExifTagValue  ; TAG structure for ExifTags, etc. 
    Number.i           ;
    Name$              ;
    Caption$           ; .. display text (different language, maybe in future) 
    Format.i           ; .. data format (type) of TAG 
    Private.i          ; .. IsPrivate = #False or #True ?? 
    ; 
    AddressOffset.i    ; .. address offset of the tag in the memory 
    AddressSize.i      ; .. address size/length of the tag in the memory 
    ; 
    Value.i            ; .. Value, different types are supported ??  
    Value$             ;  \ __ quick solution, needs some improvement 
    Array Vals.i(0)    ;  /
  EndStructure 


  ; ---------------------------------------------------------------------------
  ; Module Global Variables 
  ; 
  Global *ExifData.TByteArray                  ; entire file is stored in this memory 
  Global NewList ResultValues.TExifTagValue()  ; found tags need a place to wait 

  Global Dim ExifTagTable.TExifTagEntry(0)     ; the table of TAGs we can use 


; ---== Simple Helpers ==----------------------------------------------------------------------------------------------

  Macro DQ 
    " 
  EndMacro 

  Macro ByteToHex(_Value_) 
    "0x" + RSet(Hex(_Value_, #PB_Byte), 2, "0") + ", (" + Str(_Value_) + ")" 
  EndMacro 

  Macro WordToHex(_Value_) 
    "0x" + RSet(Hex(_Value_, #PB_Word), 4, "0") + ", (" + Str(_Value_) + ")" 
  EndMacro 

  Macro LongToHex(_Value_) 
    "0x" + RSet(Hex(_Value_, #PB_Long), 8, "0") + ", (" + Str(_Value_) + ")" 
  EndMacro 

  Macro IntToHex(_Value_) 
    "0x" + RSet(Hex(_Value_, #PB_Quad), 16, "0") + ", (" + Str(_Value_) + ")" 
  EndMacro 



; ---== Fetch BYTE, WORD, LONG, ASCII from *Memory ==------------------------------------------------------------------

  ; Little Endian (LE) == 8, 0; Big Endian (BE) == 0, 8 
  Global Dim ByteOrderWord(1, 1)  ; (ByteOrderLE, Offset) 
    ByteOrderWord(0, 0) = 8 : ByteOrderWord(0, 1) = 0   ; <-- BE 
    ByteOrderWord(1, 0) = 0 : ByteOrderWord(1, 1) = 8   ; <-- LE 

  ; Little Endian (LE) == 24, 16, 8, 0; Big Endian (BE) == 0, 8, 16, 24 
  Global Dim ByteOrderLong(1, 3)  ; (ByteOrderLE, Offset) 
    ByteOrderLong(0, 0) = 24 : ByteOrderLong(0, 1) = 16 : ByteOrderLong(0, 2) =  8 : ByteOrderLong(0, 3) =  0 ; <-- BE 
    ByteOrderLong(1, 0) =  0 : ByteOrderLong(1, 1) =  8 : ByteOrderLong(1, 2) = 16 : ByteOrderLong(1, 3) = 24 ; <-- LE 

  ; ---------------------------------------------------------------------------

  Procedure.i FetchByte(Offset)  ; return $00 
    ProcedureReturn *ExifData\Byte[Offset] 
  EndProcedure 

  ; ---------------------------------------------------------------------------

  Procedure.i FetchWord(Offset, ByteOrderLE=0)  ; returns $00 00 
    ProcedureReturn *ExifData\Byte[Offset + 0] << ByteOrderWord(ByteOrderLE, 0) + 
                    *ExifData\Byte[Offset + 1] << ByteOrderWord(ByteOrderLE, 1) 
  EndProcedure 

  ; ---------------------------------------------------------------------------

  Procedure.i FetchLong(Offset, ByteOrderLE=0)  ; returns $00 00 00 00 
    ProcedureReturn *ExifData\Byte[Offset + 0] << ByteOrderLong(ByteOrderLE, 0) + 
                    *ExifData\Byte[Offset + 1] << ByteOrderLong(ByteOrderLE, 1) + 
                    *ExifData\Byte[Offset + 2] << ByteOrderLong(ByteOrderLE, 2) + 
                    *ExifData\Byte[Offset + 3] << ByteOrderLong(ByteOrderLE, 3) 
  EndProcedure 

  ; ---------------------------------------------------------------------------

  Procedure.s FetchAscii(Offset, Length)  ; returns value$[Length] 
    ProcedureReturn PeekS(*ExifData + Offset, Length, #PB_Ascii)  
  EndProcedure 


  ; ---------------------------------------------------------------------------
  ; 
  Procedure.s GetByteOrderName(ByteOrder) 
    Select ByteOrder 
      Case #ExifByteOrder_Motorola : ProcedureReturn "Motorola" 
      Case #ExifByteOrder_Intel    : ProcedureReturn "Intel" 
    EndSelect 
    ProcedureReturn "" 
  EndProcedure 

  ; ---------------------------------------------------------------------------
  ; 
  Procedure LogOut(Message$)  ; @ Todo: further improvements 

    Debug "LOG -> " + Message$ 

  EndProcedure 


  ; ---------------------------------------------------------------------------
  ; temp marco for use in the next procedure only 
  ; 
  Macro _setTagEntry(_ConstantName_, _Private_) ; _Private_ == #False or #True 
    ExifTagTable(Index)\Number  = _ConstantName_ 
    ExifTagTable(Index)\Name$   = Mid(DQ#_ConstantName_#DQ, 11) ; cut off #ExifTAG_ constant prefix 
    ExifTagTable(Index)\Private = _Private_ 
    Index + 1 
  EndMacro 
  ; 
  Procedure InitializeExifTagTable()  ; fill the table with supported TAGs 
    Protected Index = 0 

    Dim ExifTagTable(#ExifTagTableSize)   ; constant avoid redim at the end 

    ; internal used constants 
    _setTagEntry(#ExifTAG_Unsupported        , #False)  ; internal stuff ?? 

    _setTagEntry(#ExifTAG_ImageWidth         , #False)  
    _setTagEntry(#ExifTAG_ImageHeight        , #False)  
    _setTagEntry(#ExifTAG_BitsPerSample      , #False)  
    _setTagEntry(#ExifTAG_Compression        , #False)  

    _setTagEntry(#ExifTAG_Orientation        , #False) 
    _setTagEntry(#ExifTAG_XResolution        , #False) 
    _setTagEntry(#ExifTAG_YResolution        , #False) 
    _setTagEntry(#ExifTAG_ResolutionUnit     , #False) 

    _setTagEntry(#ExifTAG_ImageDescription   , #False)  ; new 

    _setTagEntry(#ExifTAG_Make               , #False) 
    _setTagEntry(#ExifTAG_Model              , #False) 
    _setTagEntry(#ExifTAG_Software           , #False) 
    _setTagEntry(#ExifTAG_DateTime           , #False) 

    _setTagEntry(#ExifTAG_Artist             , #False) 
    _setTagEntry(#ExifTAG_Copyright          , #False) 

    _setTagEntry(#ExifTAG_ExifIFD            , #True)   ; offset to IFD (Image File Directory) 
    _setTagEntry(#ExifTAG_ExifVersion        , #False) 

    _setTagEntry(#ExifTAG_ExifFlashpixVersion, #False) 

    _setTagEntry(#ExifTAG_DateTimeOriginal   , #False) 
    _setTagEntry(#ExifTAG_DateTimeDigitized  , #False) 
    _setTagEntry(#ExifTAG_OffsetTime         , #False) 
    _setTagEntry(#ExifTAG_OffsetTimeOriginal , #False) 
    _setTagEntry(#ExifTAG_OffsetTimeDigitized, #False) 

    _setTagEntry(#ExifTAG_SubsecTime         , #False) 
    _setTagEntry(#ExifTAG_SubsecTimeOriginal , #False) 
    _setTagEntry(#ExifTAG_SubsecTimeDigitized, #False) 
    _setTagEntry(#ExifTAG_ColorSpace         , #False) 
    _setTagEntry(#ExifTAG_PixelXDimension    , #False) 
    _setTagEntry(#ExifTAG_PixelYDimension    , #False) 

    If Index - 1 <> #ExifTagTableSize 
      Debug "INTERNAL INFO: ARRAY SIZE is redimmed to " + Str(Index-1) + "  constant = " + #ExifTagTableSize 

      ; optimize the memory usage 
      ReDim ExifTagTable(Index-1)  ; the number of Tags we can use 
    EndIf 

    ; for binary search the numbers must be sorted 
    SortStructuredArray(ExifTagTable(), 0, OffsetOf(TExifTagEntry\Number), TypeOf(TExifTagEntry\Number)) 
    
    ; Debug #LF$+"Show ExifTagTable  " 
    ; For Index = 0 To #ExifTagTableSize 
    ;   Debug "  " + index + ".  " + ExifTagTable(Index)\Name$ + ", 0x" + Hex(ExifTagTable(Index)\Number) + ", " + ExifTagTable(Index)\Private 
    ; Next Index 
    ; Debug "" 
  EndProcedure 
  ; 
  UndefineMacro _setTagEntry 
  ; 
  InitializeExifTagTable()  ; call it directly to fill the arrays  

  ; ---------------------------------------------------------------------------
  ; 
  Procedure.i GetExifTagIndex(Number)  ; returns index or -1 
    ; >> Iterative Binary Search -- faster than Linear or Sequential search on sorted arrays 
    Protected retIdx, firstIdx, lastIdx, midIdx                               ;:Debug #LF$+#PB_Compiler_Procedure+"(0x"+Hex(Number)+")", 9 

    retIdx   = -1                                      ; default return value == -1 
    firstIdx =  0                                      ; start search with entire array 
    lastIdx  = ArraySize(ExifTagTable())               ;   -"- 

    While lastIdx - firstIdx > 1 
      midIdx = (lastIdx + firstIdx) / 2                                       ;:Debug "  iterate: " + firstIdx + ", " + lastIdx + ", " + midIdx, 9 
      If ExifTagTable(midIdx)\Number < Number 
        firstIdx = midIdx + 1 
      Else 
        lastIdx = midIdx 
      EndIf 
    Wend 
    If ExifTagTable(firstIdx)\Number = Number 
      retIdx = firstIdx 
    ElseIf ExifTagTable(lastIdx)\Number = Number 
      retIdx = lastIdx 
    EndIf                                                                     ;:Debug "Found 0x" + Hex(Number) + " at Index " + retIdx, 9 
    ProcedureReturn retIdx  ; return -1 (not found) or index (0..ArraySize()) 
  EndProcedure 

  ; ---------------------------------------------------------------------------

  Procedure.i ParseTagValue(entryOffset, tiffStart, byteOrderLE)  
    Protected fmt, numValues, valueOffset, offset, n, numerator, denominator  :Debug #LF$+#PB_Compiler_Procedure+"()", 9 
    Protected ret_val, tmp$, v.f 

    fmt         = FetchWord(entryOffset + 2, byteOrderLE)              ;.. acc. to spec.  
    numValues   = FetchLong(entryOffset + 4, byteOrderLE)              ;.. 
    valueOffset = FetchLong(entryOffset + 8, byteOrderLE) + tiffStart  ;.. 

    ResultValues()\Format = fmt  ; 

    Select fmt  
      Case 1, 7  ;// 1 -> byte, 8-bit unsigned int .. 7 -> undefined, 8-bit byte, value depending on field 
        If numValues = 1 
          ResultValues()\Value = FetchByte(entryOffset + 8) 
        Else  
          If numValues > 4 : offset = valueOffset : Else : offset = entryOffset + 8 : EndIf 
          ResultValues()\Value$ = FetchAscii(offset, numValues)  ; numValues == 4 
        EndIf 
        ; keep the position (address) of the value to overwrite with new data ???? 
        ResultValues()\AddressOffset = offset 
        ResultValues()\AddressSize = numValues 
        ret_val = #True 

      Case 2   ;// 2 -> ascii, 8-bit byte 
        If numValues > 4 : offset = valueOffset : Else : offset = entryOffset + 8 : EndIf 
        ResultValues()\Value$ = FetchAscii(offset, numValues - 1) 

        ; keep the position (address) of the value to overwrite with new data ???? 
        ResultValues()\AddressOffset = offset 
        ResultValues()\AddressSize = numValues - 1  ; different (because no trailing ZERO) 
        ret_val = #True 

      Case 3   ;// 3  -> short, 16 bit int 
        If numValues = 1  
          ResultValues()\Value = FetchWord(entryOffset + 8, byteOrderLE) 
        Else 
          If numValues > 2 : offset = valueOffset : Else : offset = entryOffset + 8 : EndIf 
          ReDim ResultValues()\Vals(numValues) 
          For n = 0 To numValues - 1  
            ResultValues()\Vals(n) = FetchWord(offset + 2 * n, byteOrderLE) 
          Next n 
        EndIf 
        ; keep the position (address) of the value to overwrite with new data ???? 
        ResultValues()\AddressOffset = offset 
        ResultValues()\AddressSize = numValues - 1 
        ret_val = #True 

      Case 4    ;// 4 -> long, 32 bit int 
        If numValues = 1 
          ResultValues()\Value = FetchLong(entryOffset + 8, byteOrderLE) 
        Else  
          ReDim ResultValues()\Vals(numValues) 
          For n = 0 To numValues - 1  
            ResultValues()\Vals(n) = FetchLong(offset + 4 * n, byteOrderLE) 
          Next n 
        EndIf 
        ; keep the position (address) of the value to overwrite with new data ???? 
        ResultValues()\AddressOffset = offset 
        ResultValues()\AddressSize = numValues - 1 
        ret_val = #True 

      Case 5        ;// 5 -> rational = two long values, first is numerator, second is denominator
        If numValues = 1 
          numerator = FetchLong(valueOffset, byteOrderLE) 
          denominator = FetchLong(valueOffset + 4, byteOrderLE) 
          v = numerator / denominator 
          ResultValues()\Value$ = StrF(v) 
        Else  
          For n = 0 To numValues - 1 
            numerator = FetchLong(valueOffset + 8*n, byteOrderLE) 
            denominator = FetchLong(valueOffset + 4 + 8*n, byteOrderLE) 
            v = numerator / denominator 
            ResultValues()\Value$ + StrF(v) + ";"  ; ?? 
          Next n 
        EndIf 
       ;ResultValues()\Private = #True ; don't share, not verified by now 
        ret_val = #False  ; not supported yet. 

      Case 9  ;// 9 ; slong, 32 bit signed int 
        If numValues = 1 
          ResultValues()\Value = FetchLong(entryOffset + 8, byteOrderLE) & $FFFF ; ?? 
        Else 
          ReDim ResultValues()\Vals(numValues) 
          For n = 0 To numValues - 1 
            ResultValues()\Vals(n) = FetchLong(offset + 4 * n, byteOrderLE) & $FFFF ; ?? 
          Next n 
        EndIf 
        ; keep the position (address) of the value to overwrite with new data ???? 
        ResultValues()\AddressOffset = offset 
        ResultValues()\AddressSize = numValues - 1 
        ret_val = #True 

      Case 10   ;// 10 -> signed rational, two slongs, first is numerator, second is denominator 
        If numValues = 1 
          ResultValues()\Value = FetchLong(valueOffset, byteOrderLE) / FetchLong(valueOffset+4, byteOrderLE)  ;?? 
        Else 
          ReDim ResultValues()\Vals(numValues) 
          For n = 0 To numValues - 1 
            ResultValues()\Vals(n) = FetchLong(valueOffset + 8*n, byteOrderLE) / FetchLong(valueOffset+4 + 8*n, byteOrderLE)  ;?? 
          Next n 
        EndIf 
       ;ResultValues()\Private = #True ; don't share, not verified by now 
        ret_val = #False  ; not supported yet. 

    EndSelect 
    ProcedureReturn ret_val 
  EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

  Procedure.i ParseTags(tiffStart, dirStart, byteOrderLE)  ; 
    Protected entries, entryOffset, tag, ii, idx, name$                             :Debug #LF$+#PB_Compiler_Procedure+"()", 9 
    Protected ret_val 

    entries = FetchWord(dirStart, byteOrderLE)                                 :Debug "  entries = " + entries, 9 
    For ii = 0 To entries - 1 
      entryOffset = dirStart + ii * 12 + 2   ;.. calulation  acc. to specification -- jump over unsupported tags  

      tag = FetchWord(entryOffset, byteOrderLE)   
      idx = GetExifTagIndex(tag)  ; look for TAG in the ExifTagTable() 
      If idx = -1 
; ##_TAG_## 
;       ; Log all available but not supported Tags 
;       LogOut("TAG: " + WordToHex(tag) + " not supported!") 
        Continue ; with the next tag 
      EndIf 

      ; work on supported TAGs 
      AddElement(ResultValues()) 
      ResultValues()\Number = tag                        ; copy TAG Number ... 
      ResultValues()\Name$  = ExifTagTable(idx)\Name$    ; ... and name$ 
      ResultValues()\Private = ExifTagTable(idx)\Private ; ... and private flag  

    ;;LogOut("TAG: " + WordToHex(tag) + "  " + ExifTagTable(idx)\Name$ + " supported!") 

      ret_val | ParseTagValue(entryOffset, tiffStart, byteOrderLE)  ; one valid tag is enough :) 
    Next ii 
    ProcedureReturn ret_val 
  EndProcedure 

; -----------------------------------------------------------------------------

  Procedure.i ParseEXIFData(Start) 
    Protected byteOrderLE, tags, tag, exifData, gpsData, tiffOffset            :Debug #LF$+#PB_Compiler_Procedure+"()", 9 
    Protected firstIFD_offset, ExifIFD_offset 
    Protected ret_val, rc 

    If FetchAscii(Start, 4) <> "Exif" 
      LogOut("Not valid EXIF data! " + FetchAscii(Start, 4)) 
      ProcedureReturn #False  
    EndIf 

    tiffOffset = Start + 6  ; kept for further investigation 

    ; test for TIFF validity and byte order 
    If FetchWord(tiffOffset) = #ExifByteOderMark_Intel         ; 0x4949 == Intel Byte Order 
      byteOrderLE = #True  ; ..                                            \-> little endian 
    ElseIf FetchWord(tiffOffset) = #ExifByteOderMark_Motorola  ; 0x4D4D == Motorola Byte Order 
      byteOrderLE = #False  ; ..                                           \-> big endian 
    Else  
      LogOut("Not valid TIFF data! (no 0x4949 or 0x4D4D)") 
      ProcedureReturn #False  ;.. failure because of unknown byteorder! 
    EndIf 

    If FetchWord(tiffOffset + 2, byteOrderLE) <> #TIFF_TAG_Mark  ; 0x002A  == TIFF_TAG_Mark 
      LogOut("Not valid TIFF data! (no 0x002A)") 
      ProcedureReturn #False  
    EndIf 

    firstIFD_offset = FetchLong(tiffOffset + 4, byteOrderLE) 
    If firstIFD_offset < #TIFF_FirstIFDOffset                   ; == 0x00000008 (Default offset) 
      LogOut("Not valid TIFF data! (First offset less than 8)  " + FetchLong(tiffOffset + 4, byteOrderLE)) 
      ProcedureReturn #False ; failure 
    EndIf 

    ret_val = ParseTags(tiffOffset, tiffOffset + firstIFD_offset, byteOrderLE)    ;; ### TiffTags --> ARRAY or MAP ???? 
    If ret_val 
      If ResultValues()\Number = #ExifTAG_ExifIFD ; <--> 0x8769 == ExifIFD pointer 
        ExifIFD_offset = ResultValues()\Value 
; Debug "HINT:    ExifIFD-Offset = " + ExifIFD_offset + "  // parse tags " 
        rc = ParseTags(tiffOffset, tiffOffset + ExifIFD_offset, byteOrderLE)  ; 
        ; .. needs some further investigation .. 
      EndIf 
    EndIf 
    ProcedureReturn ret_val 
  EndProcedure 


; ---== Read, Write, Free ImageFile and *Memory ==---------------------------------------------------------------------

  Procedure.i ReadExifDataFromFile(FileName$)  ; 
    Protected FILE, memsize, bytes                                                  :Debug #LF$+#PB_Compiler_Procedure+"("+FileName$+")", 9 
    Protected offset, marker 

    FreeExifData() 
    ClearList(ResultValues())  

    FILE = ReadFile(#PB_Any, FileName$) ; read with no flags 
    If FILE 
      memsize = Lof(FILE) ; Lof .. Length of (opened) file 
      *ExifData = AllocateMemory(memsize)    ; returns the address, or zero if the memory cannot be allocated 
      If *ExifData 
        bytes = ReadData(FILE, *ExifData, memsize)  ; read all data into memory block 
        LogOut("Read file with length of " + Str(bytes) + " bytes. ") 
      EndIf 
      CloseFile(FILE) 
    Else 
      *ExifData = 0 ; 
      LogOut("ERROR: Couldn't open the file '" + FileName$ + "'") 
      ProcedureReturn #False ; not a valid image (jpeg) file 
    EndIf 

    ; analyze file from the beginning 
    If FetchByte(0) <> #JM_Start Or FetchByte(1) <> #JM_SOI  ;.. == FFD8 
      LogOut("Not a valid JPEG") 
      ProcedureReturn #False ; not a valid jpeg 
    EndIf 

    offset = 2  ; jump over the first two bytes :) 
    While offset < memsize  ; scan the file memory byte by byte for find the marker 0xFFE1 
      If FetchByte(offset) <> #JM_Start 
        LogOut("Not a valid marker at offset " + offset + ", found: " + FetchByte(offset)) 
        ProcedureReturn #False  ; not a valid marker, something is wrong 
      EndIf 

      marker = FetchByte(offset + 1) 

      ; we could implement handling for other markers here, but we're only looking for 0xFFE1 for EXIF data  

      If marker = #JM_APP1  ; == 225 = $E1 
        LogOut("Found 0xFFE1 marker") 
        ProcedureReturn ParseEXIFData(offset + 4) 
      Else 
        offset + 2 + FetchWord(offset + 2) 
      EndIf 
    Wend 
    ProcedureReturn #False  ; return failure, could not found a valid marker 
  EndProcedure 

  ; ---------------------------------------------------------------------------

  Procedure FreeExifData()  ; 
    If *ExifData <> 0                                                          :Debug #LF$+#PB_Compiler_Procedure+"() // in use, clear it first.", 9 
      FreeMemory(*ExifData)  ; in use, free memory first 
      *ExifData = 0 
    EndIf 
  EndProcedure 

  ; ---------------------------------------------------------------------------

  Procedure ShowResultsOnGadget(Gadget)  ; Gadget is a #ListIcon and has two (2) columns 
    Protected txt$, ii 

    If IsGadget(Gadget) And GadgetType(Gadget) = #PB_GadgetType_ListIcon And GetGadgetAttribute(Gadget, #PB_ListIcon_ColumnCount) = 2 
;     Debug #LF$+"ResultValues(): " 
      ForEach ResultValues() 
;       Debug "  " + ResultValues()\Name$ + "  " + ResultValues()\Value + " | '" + ResultValues()\Value$ + "' | ... " 
        If ResultValues()\Private = #True  ; <-->  for internal use only :) 
          Continue  
        EndIf 
;       Debug "  " + ResultValues()\Name$ + "  " + ResultValues()\Value + " | '" + ResultValues()\Value$ + "' | ... " 

        If ResultValues()\Value$ <> "" 
          txt$ = ResultValues()\Value$
        ElseIf ResultValues()\Value <> 0 
          txt$ = Str(ResultValues()\Value) 
        Else 
          For ii = 0 To ArraySize(ResultValues()\Vals()) - 1 
            txt$ + Str(ResultValues()\Vals(ii)) + ";" 
          Next ii 
        EndIf 

        AddGadgetItem(Gadget, -1, ResultValues()\Name$ + #LF$ + txt$)  
      Next 
    Else ; not the correct gadget 
      Debug "INTERNAL: Gadget " + Gadget + " is not the correct gadget type. " 
    EndIf 
  EndProcedure 

  ; ---------------------------------------------------------------------------

  Procedure.i GetExifTagAsInteger(ExifTag, DefaultValue = -1) 
    ForEach ResultValues() 
      If ResultValues()\Number = ExifTag 
        ProcedureReturn ResultValues()\Value 
      EndIf 
    Next 
    ProcedureReturn DefaultValue 
  EndProcedure 

  Procedure.s GetExifTagAsString(ExifTag, DefaultValue$ = "") 
    ForEach ResultValues() 
      If ResultValues()\Number = ExifTag 
        ProcedureReturn ResultValues()\Value$ 
      EndIf 
    Next 
    ProcedureReturn DefaultValue$ 
  EndProcedure 

; Procedure GetExifTagValue(ExifTag) 
; EndProcedure 

EndModule ; <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<



; ---== MainWindow ==--------------------------------------------------------------------------------------------------

Procedure ShowImagePreview(FileName$)  ; update all UI gadgets 
  Protected IMAGE 
  Protected ix, iy, iw, ih, gw, gh, txt$  

  gw = GadgetWidth(#GADGET_CnvPreView)   ; <-- get the size of the image gadget 
  gh = GadgetHeight(#GADGET_CnvPreView) 

  If FileSize(FileName$) > 0  ; FileName$ = "" returns -1 as well 
    IMAGE = LoadImage(#PB_Any, FileName$)  ; <-- load image 
  EndIf 

  If IMAGE 
    iw = ImageWidth(IMAGE) 
    ih = ImageHeight(IMAGE) 
    txt$ = "Image Size = " + Str(iw) + " x " + Str(ih) 

    ; calc factor to reduce to the available gadget size 
    ix = 1 : While iw/ix > gw : ix + 1 : Wend 
    iy = 1 : While ih/iy > gh : iy + 1 : Wend 
    If ix < iy : ix = iy : EndIf  ; the bigger the better :) 

    iw / ix : ih / ix  ; shrink the size 

    ; center in hori and verti orientation 
    ix = (gw - iw) / 2  : If ix < 0 : ix = 0 : EndIf 
    iy = (gh - ih) / 2  : If iy < 0 : iy = 0 : EndIf 
  Else  ; <-- default ?? 
    txt$ = "Image: Nothing selected or found!" 
  EndIf 

  If StartDrawing(CanvasOutput(#GADGET_CnvPreView)) 
    Box(0, 0, gw, gh, #White) 
    If IMAGE  ; <-- valid image 
      DrawImage(ImageID(IMAGE), ix, iy, iw, ih) 
    EndIf 
    DrawingMode(#PB_2DDrawing_Transparent) 
    DrawText(4, 4, txt$, #Blue) 
    StopDrawing() 
  EndIf 

  If IMAGE 
    FreeImage(IMAGE) 
  EndIf 
EndProcedure 

;­-----------------------------------------------------------------------------

Procedure UpdateImage(FileName$)  ; 
  If FileName$ And ExifData::ReadExifDataFromFile(FileName$) 
    ExifData::ShowResultsOnGadget(#GADGET_LstImageInfo) 
    ExifData::FreeExifData() 
  Else  ; do some info 
    AddGadgetItem(#GADGET_LstImageInfo, -1, "No Exif-Info!")  
  EndIf 
  ShowImagePreview(FileName$)  
EndProcedure 

; -----------------------------------------------------------------------------

Procedure OpenMainWindow(WndW, WndH)  ; 
  If OpenWindow(#WINDOW_Main, 8, 8, WndW, WndH, #MainCaption$, #PB_Window_SystemMenu|#PB_Window_ScreenCentered) 
    StickyWindow(#WINDOW_Main, 1)  ; always on top, my sreen is a mess :) 

    ExplorerListGadget(#GADGET_ExpImageFiles, 4, 4, 240, WndH-8, "C:\*.jpg", #PB_Explorer_AlwaysShowSelection) 
    RemoveGadgetColumn(#GADGET_ExpImageFiles, 1) ; we don't need the other columns 
    RemoveGadgetColumn(#GADGET_ExpImageFiles, 1) ; -"- 
    RemoveGadgetColumn(#GADGET_ExpImageFiles, 1) ; -"- 
    SetGadgetItemAttribute(#GADGET_ExpImageFiles, 0, #PB_Explorer_ColumnWidth, 216, 0) 

    ListIconGadget(#GADGET_LstImageInfo, 248, 4, 320, WndH-8, "Name", 120, #PB_ListIcon_FullRowSelect | #PB_ListIcon_AlwaysShowSelection | #PB_ListIcon_GridLines) 
    AddGadgetColumn(#GADGET_LstImageInfo, 1, "Value", 200-24)  

  ; CanvasGadget(#GADGET_CnvPreView, 568+8, 8, WndW - 568-16, WndH-16) 
    CanvasGadget(#GADGET_CnvPreView, 568+8, 8, WndW - 568-16, (WndH/2)-16) 

    EditorGadget(#GADGET_EdtPreView , 568+8, (WndH/2), WndW - 568-16, (WndH/2)-8, #PB_Editor_WordWrap|#PB_Editor_ReadOnly) 
    SetGadgetText(#GADGET_EdtPreView, "No Selected Value!") 

    ProcedureReturn 1  ; success 
  EndIf 
  ProcedureReturn 0  ; failure 
EndProcedure


; ---== main program ==------------------------------------------------------------------------------------------------

Procedure main() 
  Protected WndW = 960, WndH = 600 
  Protected index, file$  

  ExamineDesktops() 
  If DesktopWidth(0) < WndW Or DesktopHeight(0) < WndH 
    MessageRequester("Display Information", "Your current resolution is to small for this application!") 
    WndW = 800 : WndH = 400 
  EndIf 

  If OpenMainWindow(WndW, WndH) 
    If FileSize("C:\Temp\camera") = -2 ; existing directory ... some test pictures exists on my computer 
      SetGadgetText(#GADGET_ExpImageFiles, "C:\Temp\camera\*.jpg") 
    EndIf 

    Repeat  ; <--- main loop --- 
      Select WaitWindowEvent() 
        Case #PB_Event_CloseWindow 
          Break   ; say good bye 

        Case #PB_Event_Gadget 
          Select EventGadget() 
            Case #GADGET_ExpImageFiles  ; Explorer with Images 
              Select EventType()  
               ;Case #PB_EventType_LeftClick 
                Case #PB_EventType_Change 
                  ClearGadgetItems(#GADGET_LstImageInfo) 
                  index = GetGadgetState(#GADGET_ExpImageFiles) 
                  If index > -1 
                    file$ = GetGadgetText(#GADGET_ExpImageFiles) + GetGadgetItemText(#GADGET_ExpImageFiles, index, 0) 
                  Else  ; update image section anyway 
                    file$ = "" 
                  EndIf 
                  UpdateImage(file$) 
                  SetGadgetText(#GADGET_EdtPreView, "No Selected Value!") 
              EndSelect 
            Case #GADGET_LstImageInfo  ; Info of selected Image 
              Select EventType()  
                Case #PB_EventType_Change 
                  index = GetGadgetState(#GADGET_LstImageInfo) 
                  If index > -1 
                    SetGadgetText(#GADGET_EdtPreView, GetGadgetItemText(#GADGET_LstImageInfo, index, 1)) 
                  Else 
                    SetGadgetText(#GADGET_EdtPreView, "No Selected Value!") 
                  EndIf 
              EndSelect 
              ; 
          EndSelect ; EventGadget() 
      EndSelect ; WaitWindowEvent() 
    ForEver ; <-- main loop end 
  EndIf ; OpenMainWindow() 
  ProcedureReturn 0 
EndProcedure 

End main() 

;----== Bottom of File ==----------------------------------------------------------------------------------------------

Tiny small Update to Version 0.02
You will find all information in the changelog.
Last edited by Axolotl on Fri Jun 23, 2023 1:12 pm, edited 1 time in total.
Just because it worked doesn't mean it works.
PureBasic 6.04 (x86) and <latest stable version and current alpha/beta> (x64) on Windows 11 Home. Now started with Linux (VM: Ubuntu 22.04).
User avatar
Kwai chang caine
Always Here
Always Here
Posts: 5526
Joined: Sun Nov 05, 2006 11:42 pm
Location: Lyon - France

Re: Reading Exif Data without foreign libraries or programs

Post by Kwai chang caine »

Waoooouuuuuhh !!! :shock:
Splendid code works nice here on W10 v5.73
Thanks a lot for sharing 8)
ImageThe happiness is a road...
Not a destination
User avatar
kenmo
Addict
Addict
Posts: 2069
Joined: Tue Dec 23, 2003 3:54 am

Re: Reading Exif Data without foreign libraries or programs

Post by kenmo »

Thank you for this!!! Useful for batch renaming JPEGs to their original timestamp, rather than "IMG_1234.jpg" or whatever.
Axolotl
Addict
Addict
Posts: 897
Joined: Wed Dec 31, 2008 3:36 pm

Re: Reading Exif Data without foreign libraries or programs

Post by Axolotl »

thank you for the answer.
Hopefully many can use it, even if the feedback has been rather low.
I like to support this great community with meaningful and new contributions. Or what i think is...

Happy coding and stay healthy.
Just because it worked doesn't mean it works.
PureBasic 6.04 (x86) and <latest stable version and current alpha/beta> (x64) on Windows 11 Home. Now started with Linux (VM: Ubuntu 22.04).
User avatar
Caronte3D
Addict
Addict
Posts: 1379
Joined: Fri Jan 22, 2016 5:33 pm
Location: Some Universe

Re: Reading Exif Data without foreign libraries or programs

Post by Caronte3D »

Axolotl wrote: Fri May 19, 2023 10:37 am Hopefully many can use it, even if the feedback has been rather low.
Your contribution is certainly appreciated by almost everyone on this forum, just not everyone needs this feature right now (like me), but when we need something like this, we search the forum and use your code for sure, so... Don't measure the interest by the number of posts here :wink:
User avatar
mk-soft
Always Here
Always Here
Posts: 6411
Joined: Fri May 12, 2006 6:51 pm
Location: Germany

Re: Reading Exif Data without foreign libraries or programs

Post by mk-soft »

Works fine :wink:

OS: macOS (change explorer path)
My Projects ThreadToGUI / OOP-BaseClass / EventDesigner V3
PB v3.30 / v5.75 - OS Mac Mini OSX 10.xx - VM Window Pro / Linux Ubuntu
Downloads on my Webspace / OneDrive
Axolotl
Addict
Addict
Posts: 897
Joined: Wed Dec 31, 2008 3:36 pm

Re: Reading Exif Data without foreign libraries or programs

Post by Axolotl »

Thanks guys,

and I'm not complaining, just making an observation.
Just because it worked doesn't mean it works.
PureBasic 6.04 (x86) and <latest stable version and current alpha/beta> (x64) on Windows 11 Home. Now started with Linux (VM: Ubuntu 22.04).
Axolotl
Addict
Addict
Posts: 897
Joined: Wed Dec 31, 2008 3:36 pm

Re: Reading Exif Data without foreign libraries or programs

Post by Axolotl »

So I added a new TAG called ImageDescription to the supported tag values.
See code above.
Just because it worked doesn't mean it works.
PureBasic 6.04 (x86) and <latest stable version and current alpha/beta> (x64) on Windows 11 Home. Now started with Linux (VM: Ubuntu 22.04).
Post Reply