EXIF data with PowerShell (Part 2)

Last week I talked about accessing the EXIF property values embedded into JPEGs in this post using PowerShell to be able to audit it for privacy concerns. At the end of blog post if running the function Get-ExifContents on an open file handle and given an EXIF tag the following output would be displayed:
As can be seen in the screenshot my camera maker is a Huawei at the output of the first call to Get-ExifContents with the tag of 271. Tag 1 is the reference to the latitude in tag 2, both can be seen in the following output. PowerShell converted the rational 64-bit unsigned integer into some kind of printable characters because it was converted into an ASCII string in the Get-ExifContents function. To get around that, I had to write another function to keep from getting the property value as a string and to convert it from the byte array that it is retrieved as. According to this website, the GPS coordinates are saved as rational64u. A rational64u is six unsigned 32-bit integers saved in an array, which can be seen here:

To convert the value to the GPS coordinates, some math is required, you divide the first integer by the second and so on with the other four integers. That will give the coordinates in the degrees, minutes, and seconds format like 41°24'12.2"N, the N at the end denotes North and can be found by EXIF Tag 1 which is GPSLatitudeRef. To convert that to Decimal Degrees the following formula is used: dd = degress + (minutes / 60) + (seconds / 3600). Once put together the following PowerShell function will get the property value from the EXIF metadata for latitude and longitude and return it in the decimal degrees format.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Function Get-Coordinates{
     param($image, $exifCode)
     Try {
          $propertyItem = $image.GetPropertyItem($exifCode)
          $valueBytes = $propertyItem.value
          [double]$degree = (([System.BitConverter]::ToInt32($valueBytes, 0)) / ([System.BitConverter]::ToInt32($valueBytes,4)))
          [double]$minute = (([System.BitConverter]::ToInt32($valueBytes, 8)) / ([System.BitConverter]::ToInt32($valueBytes,12)))
          [double]$second = (([System.BitConverter]::ToInt32($valueBytes, 16)) / ([System.BitConverter]::ToInt32($valueBytes,20)))
          $value = $degree + ($minute / 60) + ($second / 3600)
     }
     Catch {
          $value = "<empty>"
     }
     return $value
}

The next thing to do is to call these functions, Get-ExifContents and Get-ExifCoordinates in a way that returns the data that can be manipulated in a good manner and either print it to the screen or save it in a format that can be easily parsed. I tied those functions together with a third function that will export the data into an ordered custom PowerShell object. PowerShell objects are like a C structure or Python dictionary in the way that they allow a key to be associated to a value and easily interacted with. A PowerShell object is created with $object = [pscustomobject]@{key = value,.....} the only downside is that when calling the object the key-value pairs are not ordered. To fix that after [pscustomobject] add [ordered], this will ensure that the object is created in the order that is specified when called and will always be in that order. Also in the function that calls the other two, a file handle should be created and then passed to the functions to optimize the code and cut down on unneeded lines. The following function Get-FileContents handles the file handles, calling either Get-ExifContents or Get-ExifCoordinates, saves the results as a custom PowerShell object and returns that object to the calling code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Function Get-FileContents {
     param($file)
     # Creates the full path for the file
     Try {
          $fullPath = (Resolve-Path $file).path
          # Creates a file handle to the image
          $fs = [System.IO.File]::OpenRead($fullPath)
          # Reads the image to allow parsing for EXIF data
          $image = [System.Drawing.Image]::FromStream($fs, $false, $false)
     }
     Catch {
          if (($fs) -or ($image)){
               $image.dispose()
               $fs.close()
          }
          Write-Error "Error Opening $file"
          return
     }
     $maker = Get-ExifContents -image $image -exifCode "271"
     $model = Get-ExifContents -image $image -exifCode "272"
     $version = Get-ExifContents -image $image -exifCode "305"
     $dateTime = Get-ExifContents -image $image -exifCode "306"
     $lat = Get-Coordinates -image $image -exifCode "2"
     $long = Get-Coordinates -image $image -exifCode "4"
     $latRef = Get-ExifContents -image $image -exifCode "1"
     $longRef = Get-ExifContents -image $image -exifCode "3"
     $altitude = Get-Coordinates -image $image -exifCode "6"
     # Puts all the EXIF data in a PSObject to return
     $exifData = [pscustomobject][ordered]@{
          File = $file
          CameraMaker = $maker
          CameraModel = $model
          SoftwareVersion = $version
          DateTaken = $dateTime
          Latitude = [string]$lat + $latRef
          Longitude = [string]$long + $longRef
          Altitude = $altitude
     }
     if ($exifData.Latitude -eq "<empty><empty>"){
          $exifData.Latitude = "<empty>"
     }
     if ($exifData.Longitude -eq "<empty><empty>"){
          $exifData.Longitude = "<empty>"
     }
     # releases the file handles
     $image.dispose()
     $fs.Close()
     return $exifData
}

The first line accepts a parameter as the variable $file which is the file that I want the EXIF data from. The Try code attempts to open a file handle to the full path of the input file. If the handle failed to open, it would go into the catch block and close the file handle if created, write an error message to the console, and then return to the calling code. The next several lines calls either Get-ExifContents or Get-ExifCoordinates while passing the file handle to the image file and the EXIF tag for the metadata being interrogated and saves it into a temp variable. A custom ordered PowerShell object is then created and the temp variables being saved into the object. After that, the function will close and dispose of the file handles to the JPG file and return the custom object to the calling code. The function returns the following output:

In my next post I will show how I scripted the function to pull the EXIF metadata from the file or JPGs in a directory, and either write the output to the screen in a formatted output or export it to a CSV to be parsed later.

Leave comment

Your email address will not be published. Required fields are marked with *.