Script and AppV Repository
Script and AppV Repository
Script and AppV Repository

Maybe you recognize this: you have a bunch of pictures but you do not have an idea where and when these pictures have been taken. Then my script PictureDetails is useful: put all the pictures you want to check in a folder, and the script will find the location where the picture has been taken, together with a Google Maps map. And all displayed on an HTML page.

There is one important requirement: a Google API. Instructions can be found on this website Google Maps Platform

Some background information and challenges I had.

The first challenge was to get the coordinates that Google Maps uses. So how to translate the coordinates in the picture to coordinates that Google Maps uses.

A picture has so-called metadata: that is information about the picture like camera model, light, zoom, filename, creating date etc. But there is also the EXIF data: that is information where the picture has been taken. The GPS coordinates are coded in the picture. So you have to decode the GPS coordinates from the picture.

The EXIF information is in the picture, as I said before. You have to extract the following EXIF information from the picture:

  • GPSLatitudeRef
  • GPSLatitude (rational64u)
  • GPSLongitudeRef
  • GPSLongitude (rational64u)

Let's check the GPSLatitude. It is an array that contains 21 digits, an array from [0] to [20], as type byte. The degree is calculated to divide item number 0 by item number 4. The minutes are calculated to divide item number 8 by item number 12. And the seconds to divide item number 16 by item number byte 20.


Item[0]  = [byte]52 -> [Integer]52
Item[4]  = [byte]1  -> [Integer]4
Item[8]  = [byte]30 -> [Integer]30
Item[12] = [byte]1  -> [Integer]1
Item[16] = [byte]58 -> [Integer]58
Item[20] = [byte]1  -> [Integer]1

So this one is easy to calculate: 52°, 30', 58". It is that easy as the integer values are the same as the byte values.

Now the following example:

Item[0]  = [byte]47  -> [Integer]47
Item[4]  = [byte]1   -> [Integer]1
Item[8]  = [byte]8   -> [Integer]8
Item[12] = [byte]1   -> [Integer]1
Item[16] = [byte]254 -> [Integer]-2100380930
Item[20] = [byte]0   -> [Integer]100000000

Now there is an issue. If you divide item number 16 by item number 20 you will get a negative result. And negative seconds are impossible.

And the solution.
There are two different integers: signed and unsigned. In short: a signed integer can have a negative number, an unsigned integer is always greater than 0. If you translate [byte]254 to an unsigned integer you get 2194586366. See Understanding Numbers in PowerShell for more information.

So we have a coordinate. But Google Maps does not work with GPS coordinates, it works with decimal coordinates. The decimal coordinates are calculated as follows: degree + (minutes / 60) + (seconds / 3600). So 52°, 30', 58" is 52 + (30/60) + (58/3600) = 52.5161111111111. If the GPSLatitudeRef (North or South) = South than this outcome should be multiplied by -1. If the GPSLongitudeRef (East or West) is West than the outcome should be multiplied by -1.

This is used in the function Get-EXIFDataFromJPGFile. See the PowerShell script below. Change the value for the parameter FileName accordingly.

Function Get-GPSDetails
 {
 
     <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       13-July-2020
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Get-GPSDetails
    =============================================================================================================================================
    .SYNOPSIS

    This function reads the latitude and longitude data from a JPG file. More information can be found at:
    http://www.forensicexpedition.com/2017/08/03/imagemapper-a-powershell-metadata-and-geo-maping-tool-for-images/

    More information about the EXIF layout can be found at: https://exiftool.org/TagNames/GPS.html

    #>
 
    param
     (
      [int]    $ID,
      [String] $FileNameWithGPSDetails
     )

    $img                 = New-Object -TypeName system.drawing.bitmap -ArgumentList $FileNameWithGPSDetails
    $IMGPropertyItems    = $img.PropertyItems | Where {($_.ID -eq $ID)}
    [Double]$Deg         = 0
    [Double]$Min         = 0
    [Double]$Sec         = 0
    $GPSTable            = @()

  
    For($a=0;$a -le 20;$a++)
     {
     
      $GPSRecord  = [ordered] @{"Number"                   = "";
                                "Value"                    = "";
                                "Type"                     = "";
                                "Decimal value (Signed)"   = "";
                                "Decimal value (Unsigned)" = "";
                                "Remark"                   = ""
                                }
     
      Switch ($a)
       {
        "0"  {$Remark = "Deg1"; Break}
        "4"  {$Remark = "Deg2"; Break}
        "8"  {$Remark = "Min1"; Break}
        "12" {$Remark = "Min2"; Break}
        "16" {$Remark = "Sec1"; Break}
        "20" {$Remark = "Sec2"; Break}
        Default {$Remark = ""}
       }
     
      $GPSRecord."Number"                   = $a
      $GPSRecord."Value"                    = $IMGPropertyItems.Value[$a]
      $GPSRecord."Type"                     = $IMGPropertyItems.Value[$a].Gettype()
      $GPSRecord."Decimal value (Signed)"   = ([System.BitConverter]::ToInt32($img.GetPropertyItem($ID).Value, $a))
      $GPSRecord."Decimal value (Unsigned)" = ([System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, $a))
      $GPSRecord."Remark"                   = $Remark
      $objGPSRecord                         = New-Object PSObject -Property $GPSRecord
      $GPSTable                            += $objGPSRecord
    } 
                   
    [Double]$Deg1 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 0))
    [Double]$Deg2 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 4))
    [Double]$Deg = $Deg1 / $Deg2

    [Double]$Min1 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 8))
    [Double]$Min2 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 12))
    [Double]$Min  = $Min1 / $Min2

    [Double]$Sec1 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 16))
    [Double]$Sec2 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 20))
    [Double]$Sec = $Sec1 / $Sec2

    Write-Host "$Deg1 / $Deg2 = $Deg"
    Write-Host "$Min1 / $Min2 = $Min"
    Write-Host "$Sec1 / $Sec2 = $Sec"

    Write-Host ""
    Write-Host "     --- Overview of the EXIF GPS Data ---"
    Write-Host "     --- ID $ID of $(Split-Path -Leaf $FileNameWithGPSDetails)"
    ForEach ($object in $GPSTable)
     {
       if($object."Remark")
       {
        Write-Host "       Number:                     $($object."Number")"
        Write-Host "       Value:                      $($object."Value")"
        Write-Host "       Type:                       $($object."Type")"
        Write-Host "       Decimal value (Signed)    : $($object."Decimal value (Signed)")"
        Write-Host "       Decimal value (Unsigned)  : $($object."Decimal value (Unsigned)")"
        Write-Host "       Remark:                     $($object."Remark")"
        Write-Host "       ---------------------------------------------------------------------"
       }
     }
    $GPSTable.Clear()
    
    Return [Double]$Deg, [Double]$Min, [Double]$Sec
 }


Function Get-EXIFDataFromJPGFile
   {

    <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       06-July-2020
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Get-EXIFDataFromJPGFile
    =============================================================================================================================================
    .SYNOPSIS

    This function reads the latitude and longitude data from a JPG file.
    If no valid data is found, the return codes will be 1024 and 1024.
    This code is based on 
    http://www.forensicexpedition.com/2017/08/03/imagemapper-a-powershell-metadata-and-geo-maping-tool-for-images/

    More information about the EXIF layout can be found at: https://exiftool.org/TagNames/GPS.html

    #>

    Param
     (
      [String] $FileName
     )
    
    
    $img     = New-Object -TypeName system.drawing.bitmap -ArgumentList $FileName
    $Encode  = New-Object System.Text.ASCIIEncoding
    $GPSInfo = $true
    $GPSLat  = ""
    $GPSLon  = ""
      
    # =============================================================================================================================================
    # Try to get the latitude (N or S) from the image.
    # If not successfull then this information is not in the image
    # and quit with the numbers 1024 and 1024. 
    # =============================================================================================================================================

    Try
     {
      
      $LatNS = $Encode.GetString($img.GetPropertyItem(1).Value)
     }
      Catch
     {
      $GPSInfo = $False
     }
                
    If ($GPSInfo -eq $true)
     {
      $LonEW = $Encode.GetString($img.GetPropertyItem(3).Value)
      
      [Double]$LatDeg, [Double]$LatMin, [Double]$LatSec = Get-GPSDetails -FileNameWithGPSDetails $FileName -ID 2 
      [Double]$LonDeg, [Double]$LonMin, [Double]$LonSec = Get-GPSDetails -FileNameWithGPSDetails $FileName -ID 4
    
      $GPSLat = "$([int]$LatDeg)º $([int]$LatMin)' $([int]$LatSec)$([char]34) $LatNS"
      $GPSLon = "$([int]$LonDeg)º $([int]$LonMin)' $([int]$LonSec)$([char]34) $LonEW"

      Write-Verbose "    The picture $FileName has the following GPS coordinates:"
      Write-Verbose "       $GPSLat"        
      Write-Verbose "       $GPSLon"

    # =============================================================================================================================================
    # Convert the latitude and longitude to numbers that Google Maps recognizes.
    # =============================================================================================================================================

      $LatOrt = 0
      $LonOrt = 0

      If ($LatNS -eq 'S')
       {
        $LatOrt = "-"   
       }
      If ($LonEW -eq 'W')
       {
        $LonOrt = "-"
       }

      $LatDec = ($LatDeg + ($LatMin/60) + ($LatSec/3600))
      $LonDec = ($LonDeg + ($LonMin/60) + ($LonSec/3600))

      $LatOrt = $LatOrt + $LatDec
      $LonOrt = $LonOrt + $LonDec

    # =============================================================================================================================================
    # The numbers that where returned contained a decimal comma instead of a decimal point. 
    # So the en-US culture is forced to get the correct number notation.
    # =============================================================================================================================================
    
      $LatOrt = $LatOrt.ToString([cultureinfo]::GetCultureInfo('en-US'))
      $LonOrt = $LonOrt.ToString([cultureinfo]::GetCultureInfo('en-US'))

      Write-Verbose "    The picture $FileName has the following decimal coordinates:"
      Write-Verbose "      $LatOrt"
      Write-Verbose "      $LonOrt"
    }
     else
    {
     
   # =============================================================================================================================================
   # Ohoh... No GPS information in this picture.
   # =============================================================================================================================================
     
     Write-Verbose "    The picture $FileName does not contain GPS information."
     $LatOrt = "1024"
     $LonOrt = "1024"
    }

    Return $LatOrt,$LonOrt,$GPSLat,$GPSLon
  }

  # =============================================================================================================================================
  # Add assemblies.
  # =============================================================================================================================================
  
  Clear-Host

    
  Add-Type -AssemblyName System.Web
  Add-Type -AssemblyName System.Drawing
  
  Get-EXIFDataFromJPGFile -FileName "C:\tmp\20200204_112613.jpg"

Now the coordinates stuff has been solved. Now the Google Map on an HTML table.

The first part is to load a polyfill. A polyfill is code that implements a feature on web browsers that do not support that feature. See Polyfill (programming) for more information. After that, the Google API called a function that puts the map on a page. CSS is used to have the proper format.

If you use this code and replace <GOOGLE_API_KEY> with your own key you have a good starting point:

<!DOCTYPE html>
<html>
  <head>
<script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
    <script
      src="https://maps.googleapis.com/maps/api/js?key=<GOOGLE_API_KEY>&callback=initialize&libraries=&v=weekly"
      defer
    ></script>
<script>
function initialize() {
 var myPicture1 = new google.maps.LatLng(47.555715031922222222222222222,7.5798174841888888888888888889);
 var myPicture2 = new google.maps.LatLng(47.544165608527777777777777778,7.5798381874888888888888888889);

 var OptionsPicture1 = {
 zoom:           17,
 center:         myPicture1,
 mapTypeId:      google.maps.MapTypeId.ROADMAP,
 mapTypeControl: 0
 }
 var OptionsPicture2 = {
 zoom:           17,
 center:         myPicture2,
 mapTypeId:      google.maps.MapTypeId.ROADMAP,
 mapTypeControl: 0
 }

 var mapPicture1 = new google.maps.Map(document.getElementById('css-Picture1'), OptionsPicture1);
 var mapPicture2 = new google.maps.Map(document.getElementById('css-Picture2'), OptionsPicture2);

 var MarkerPicture1 = new google.maps.Marker({
 position: myPicture1,
 map: mapPicture1,
 title: 'Picture1'
 }); 
 var MarkerPicture2 = new google.maps.Marker({
 position: myPicture2,
 map: mapPicture2,
 title: 'Picture2'
 });
} 
</script>

<style type="text/css">

#css-Picture1 {
 width: 425px;
 height: 120px;
 background-color: #CCC;
}
#css-Picture2 {
 width: 425px;
 height: 120px;
 background-color: #CCC;
}
</style>

</head>
<body>
<h1>An Example!</h1>
  <table> 
  <tr> 
    <td width="400px" height="400px">
      <div id="css-Picture1" style="width:100%; height:100%"></div>
    </td>
  </tr>
  <tr>
    <td width="400px" height="400px">
      <div id="css-Picture2" style="width:100%; height:100%"></div>
    </td>
  </tr>
</table>
</body>

The last part is reversed geocoding. You enter the coordinates and Google Maps find the address. To use that function reversed geocoding must be enabled. You can do that on https://console.cloud.google.com/apis/dashboard. Follow these instructions:

  1. Go to https://console.cloud.google.com/apis/dashboard
  2. Click on ENABLE APIS AND SERVICES
  3. In the 'Search for APIs & Services' enter 'Geocoding API'.
  4. Click on Geocoding API
  5. Click on the [Enable] button.

You can verify geocoding with this PowerShell code:

JSON:

Clear-Host
$API    = "YOUR API KEY"
$LatLng = "47.555715031922222222222222222,7.5798174841888888888888888889"
$JSON = Invoke-WebRequest "https://maps.googleapis.com/maps/api/geocode/json?latlng=$LatLng&key=$API" | ConvertFrom-Json
$JSON

XML:

Clear-Host
$API    = "YOUR API KEY"
$LatLng = "47.555715031922222222222222222,7.5798174841888888888888888889"
[xml]$XML = Invoke-WebRequest "https://maps.googleapis.com/maps/api/geocode/xml?latlng=$LatLng&key=$API"
$XML.GeocodeResponse

And finally some background information about the script.

With the information above the most has already been explained. There is an HTML table with information about the pictures. First, the table is defined in an array, called $Global:arrHTMLTable. The first column contains a link to the picture. The second column contains the pictures' metadata. The last column contains the Google Maps map or an error message if the Google Maps map cannot be created.

With the PowerShell command...

$HTMLTable = $Global:arrHTMLTable | ConvertTo-Html -Property Picture,"Photo Data","Google Maps Map" -Fragment -PreContent "<h2>The pictures</h2>"

...the HTML tables are created.

And with one simple command the whole page is created and written to disk:

$Report      = ConvertTo-Html -Head $Header -Body "$Title $HTMLTable" -PostContent $PostContent 
$Report | Out-File $HTMLFile

Now, the whole script:

<#
.SYNOPSIS
    Finds all the jpg files in a folder and puts the information in a html file. Also the location where the picture has been taken is shown
    in Google Maps if the Google Maps API key is provided. 

.DESCRIPTION
    Finds all the jpg files in a folder and puts the information in a html file. Also the location where the picture has been taken is shown
    in Google Maps if the Google Maps API key is provided. 

.EXAMPLE
    Run an inventory on all the pictures in the current directory
    ."PictureDetails_v01.ps1"

.EXAMPLE
    Run an inventory on all the pictures in the current directory and log in C:\TMP
    ."PictureDetails_v01.ps1" -LogPath c:\tmp

.EXAMPLE
    Run an inventory on all the pictures in the current directory and detailed logging in C:\TMP
    ."PictureDetails_v01.ps1" -LogPath c:\tmp -DetailedLogging

.EXAMPLE
    Run an inventory on all the pictures in the current directory and use the Google API Key 1234567890-0987654321. Include the subfolders.
    ."PictureDetails_v01.ps1" -GoogleAPIKey 1234567890-0987654321 -IncludeSubFolders

.EXAMPLE
    Run an inventory on all the pictures in the current directory and use the Google API Key 1234567890-0987654321. Include the subfolders.
    Show only the unique pictures: the filename discovered only once, all the other files with the same filename are skipped.
    ."PictureDetails_v01.ps1" -GoogleAPIKey 1234567890-0987654321 -IncludeSubFolders -ShowOnlyUniquePictures

.EXAMPLE
    Run an inventory on all the pictures in C:\Pictures and use the Google API Key 1234567890-0987654321. Include the subfolders.
    Show only the unique pictures: the filename discovered only once, all the other files with the same filename are skipped.
    ."PictureDetails_v01.ps1" -Directory C:\Pictures -GoogleAPIKey 1234567890-0987654321 -IncludeSubFolders -ShowOnlyUniquePictures

.NOTES
    Author:  Willem-Jan Vroom
    Website: https://www.vroom.cc/
    Twitter: @TheStingPilot

v0.1:
   * Initial version.

#>

[CmdletBinding(DefaultParameterSetName = 'Default')]

Param
  (
   [Parameter(HelpMessage='Specify the Google API key.')]
   [Parameter(Mandatory=$False,  ParameterSetName='Default')]
   [String]   $GoogleAPIKey = "",

   [Parameter(HelpMessage='Specify the log path.')]
   [Parameter(Mandatory=$False, ParameterSetName='Default')]
   [String]   $LogPath = "",

   [Parameter(HelpMessage='Specify the directory that contains all images.')]
   [Parameter(Mandatory=$false,  ParameterSetName = 'Default')]
   [String]   $Directory = "",

   [Parameter(HelpMessage='Enable detailed logging to a log file.')]
   [Parameter(Mandatory=$False, ParameterSetName='Default')]
   [Switch]   $DetailedLogging,

   [Parameter(HelpMessage='Include subfolders.')]
   [Parameter(Mandatory=$False, ParameterSetName='Default')]
   [Switch]   $IncludeSubFolders,

   [Parameter(HelpMessage='Show only the unique pictures.')]
   [Parameter(Mandatory=$False, ParameterSetName='Default')]
   [Switch]   $ShowOnlyUniquePictures
  )

# =============================================================================================================================================
# Function block
# =============================================================================================================================================

  Function Add-EntryToLogFile
   {

    <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       17-May-2020
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Add-EntryToLogFile
    =============================================================================================================================================
    .SYNOPSIS

    This function adds a line to a log file

    #>

    Param
     (
      [string] $Entry
     )
      
     Write-Verbose $Entry
     if($Global:DetailedLogging)
      {
       $Timestamp = (Get-Date -UFormat "%a %e %b %Y %X").ToString()
       Add-Content $Global:LogFile -Value $($Timestamp + " " + $Entry) -Force -ErrorAction SilentlyContinue
      }
   }


  Function Create-Folder
   {

    <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       03-August-2018
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Create-Folder
    =============================================================================================================================================
    .SYNOPSIS

    This function creates the given folder.
    The function returns two results:
     1. A message.
     2. True or false (success or failure)

    #>
    
    param
     (
      [String] $FolderName
     )

   $bolResult     = $True
   $ResultMessage = ""

   if(-not(Test-Path $('FileSystem::' + $FolderName)))
    {
     New-Item -Path $FolderName -ItemType Directory | Out-Null
     Sleep 1
     if(test-path $('FileSystem::' + $FolderName))
      {
       $ResultMessage = "The folder '$FolderName' has been created."
      }
       else
      {
       $ResultMessage = "Something went wrong while creating the folder '$FolderName'. "
       $bolResult     = $false
      }
    }
     else
    {
     $ResultMessage = "The folder $FolderName already exists."
    }

    Return $ResultMessage,$bolResult 

   }

  Function Add-EntryToHTMLTable
   {

    <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       03-August-2018
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Add-EntryToHTMLTable
    =============================================================================================================================================
    .SYNOPSIS

    This function adds information to a HTML table for later use.

    #>

    Param
     (
      [String]  $Picture           = "",
      [String]  $PhotoData         = "",
      [String]  $GoogleMapsMap     = ""
     )

    $Record  = [ordered] @{"Picture"                           = "";
                           "Photo Data"                        = "";
                           "Google Maps Map"                   = ""
                           }

    $Record."Picture"                   = $Picture
    $Record."Photo Data"                = $PhotoData
    $Record."Google Maps Map"           = $GoogleMapsMap
 
    $objRecord                          = New-Object PSObject -Property $Record
    $Global:arrHTMLTable               += $objRecord

    Add-EntryToLogFile -Entry  "    Picture:           $Picture"
    Add-EntryToLogFile -Entry  "    Photo Data:        $PhotoData"
    Add-EntryToLogFile -Entry  "    Google Maps Map:   $GoogleMapsMap"
    Add-EntryToLogFile -Entry  ""
    Add-EntryToLogFile -Entry  "###############################################################################################################`n"
   
   }

  Function Get-PhotoMetaDataFromJPGFile
   {

    <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       03-August-2018
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Get-PhotoDataFromJPGFile
    =============================================================================================================================================
    .SYNOPSIS

    This function reads all the metadata from a JPG file
    This function is based on Get-FileMetaDataReturnObject.ps1 from the Microsoft Scripting Guys

    #>

   Param
     (
      [String] $FileName
     )

    $tmp        = Get-ChildItem $FileName 
    $pathname   = $tmp.DirectoryName 
    $Name       = $tmp.Name 
 
    $objShell   = New-Object -ComObject Shell.Application 
    $objFolder  = $objShell.namespace($pathname) 
    $objFile    = $objFolder.parsename($Name) 
    $Results    = New-Object PSOBJECT 

    for($a=0; $a -le 1024; $a++) 
    { 
        if($objFolder.getDetailsOf($objFolder, $a) -and $objFolder.getDetailsOf($objFile, $a))  
        { 
            $hash += @{$($objFolder.getDetailsOf($objFolder, $a)) = $($objFolder.getDetailsOf($objFile, $a))} 
            $Results | Add-Member $hash -Force 
        } 
    } 

    $ReturnString = ""
    ForEach ($Result in $Results -split ";")
     {
      $ReturnString += $Result + "<br>"
     }

    $hash.Clear() 
    $ReturnString = $ReturnString -replace "@{","" -replace "}",""
    Return $ReturnString
 
  }

  Function WorkingInternetConnection
   {

    <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       09-July-2020
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Check-Internet
    =============================================================================================================================================
    .SYNOPSIS

    Check for a working internet connection.

    #>
    Try
    {
      Invoke-WebRequest -Uri "https://www.google.com" -ErrorAction SilentlyContinue
      Add-EntryToLogFile -Entry "The website google.com can be reached. Thus there is a working internet connection."
      Return $True
     }
     Catch
     {
     Add-EntryToLogFile -Entry "The website google.com cannot be reached. Thus there is no working internet connection."
     Return $False
     }
   }

Function Get-GPSDetails
 {
 
     <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       13-July-2020
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Get-GPSDetails
    =============================================================================================================================================
    .SYNOPSIS

    This function reads the latitude and longitude data from a JPG file. More information can be found at:
    http://www.forensicexpedition.com/2017/08/03/imagemapper-a-powershell-metadata-and-geo-maping-tool-for-images/

    More information about the EXIF layout can be found at: https://exiftool.org/TagNames/GPS.html

    #>
 
    param
     (
      [int]    $ID,
      [String] $FileNameWithGPSDetails
     )

    $img                 = New-Object -TypeName system.drawing.bitmap -ArgumentList $FileNameWithGPSDetails
    $IMGPropertyItems    = $img.PropertyItems | Where {($_.ID -eq $ID)}
    [Double]$Deg         = 0
    [Double]$Min         = 0
    [Double]$Sec         = 0
    $GPSTable            = @()

  
    For($a=0;$a -le 20;$a++)
     {
     
      $GPSRecord  = [ordered] @{"Number"                   = "";
                                "Value"                    = "";
                                "Type"                     = "";
                                "Decimal value (Signed)"   = "";
                                "Decimal value (Unsigned)" = "";
                                "Remark"                   = ""
                                }
     
      Switch ($a)
       {
        "0"  {$Remark = "Deg1"; Break}
        "4"  {$Remark = "Deg2"; Break}
        "8"  {$Remark = "Min1"; Break}
        "12" {$Remark = "Min2"; Break}
        "16" {$Remark = "Sec1"; Break}
        "20" {$Remark = "Sec2"; Break}
        Default {$Remark = ""}
       }
     
      $GPSRecord."Number"                   = $a
      $GPSRecord."Value"                    = $IMGPropertyItems.Value[$a]
      $GPSRecord."Type"                     = $IMGPropertyItems.Value[$a].Gettype()
      $GPSRecord."Decimal value (Signed)"   = ([System.BitConverter]::ToInt32($img.GetPropertyItem($ID).Value, $a))
      $GPSRecord."Decimal value (Unsigned)" = ([System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, $a))
      $GPSRecord."Remark"                   = $Remark
      $objGPSRecord                         = New-Object PSObject -Property $GPSRecord
      $GPSTable                            += $objGPSRecord
    } 
                   
    [Double]$Deg1 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 0))
    [Double]$Deg2 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 4))
    [Double]$Deg = $Deg1 / $Deg2

    [Double]$Min1 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 8))
    [Double]$Min2 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 12))
    [Double]$Min  = $Min1 / $Min2

    [Double]$Sec1 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 16))
    [Double]$Sec2 = ([Decimal][System.BitConverter]::ToUInt32($img.GetPropertyItem($ID).Value, 20))
    [Double]$Sec = $Sec1 / $Sec2
    
    Add-EntryToLogFile -Entry ""
    Add-EntryToLogFile -Entry "     --- Overview of the EXIF GPS Data ---"
    Add-EntryToLogFile -Entry "     --- ID $ID of $(Split-Path -Leaf $FileNameWithGPSDetails)"
    ForEach ($object in $GPSTable)
     {
      if($object."Remark")
       {
        Add-EntryToLogFile -Entry "       Number:                     $($object."Number")"
        Add-EntryToLogFile -Entry "       Value:                      $($object."Value")"
        Add-EntryToLogFile -Entry "       Type:                       $($object."Type")"
        Add-EntryToLogFile -Entry "       Decimal value (Signed)    : $($object."Decimal value (Signed)")"
        Add-EntryToLogFile -Entry "       Decimal value (Unsigned)  : $($object."Decimal value (Unsigned)")"
        Add-EntryToLogFile -Entry "       Remark:                     $($object."Remark")"
        Add-EntryToLogFile -Entry "       ---------------------------------------------------------------------"
       }
     }

    Add-EntryToLogFile "         (Unsigned decimal value)  Deg1 / Deg2   ->   $Deg1 / $Deg2 = $Deg"
    Add-EntryToLogFile "         (Unsigned decimal value)  Min1 / Min2   ->   $Min1 / $Min2 = $Min"
    Add-EntryToLogFile "         (Unsigned decimal value)  Sec1 / Sec2   ->   $Sec1 / $Sec2 = $Sec"
        
    $GPSTable.Clear()
    
    Return [Double]$Deg, [Double]$Min, [Double]$Sec
 }


Function Get-EXIFDataFromJPGFile
   {

    <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       06-July-2020
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Get-EXIFDataFromJPGFile
    =============================================================================================================================================
    .SYNOPSIS

    This function reads the latitude and longitude data from a JPG file.
    If no valid data is found, the return codes will be 1024 and 1024.
    This code is based on 
    http://www.forensicexpedition.com/2017/08/03/imagemapper-a-powershell-metadata-and-geo-maping-tool-for-images/

    More information about the EXIF layout can be found at: https://exiftool.org/TagNames/GPS.html

    #>

    Param
     (
      [String] $FileName
     )
    
    
    $img     = New-Object -TypeName system.drawing.bitmap -ArgumentList $FileName
    $Encode  = New-Object System.Text.ASCIIEncoding
    $GPSInfo = $true
    $GPSLat  = ""
    $GPSLon  = ""
      
    # =============================================================================================================================================
    # Try to get the latitude (N or S) from the image.
    # If not successfull then this information is not in the image
    # and quit with the numbers 1024 and 1024. 
    # =============================================================================================================================================

    Try
     {
      
      $LatNS = $Encode.GetString($img.GetPropertyItem(1).Value)
     }
      Catch
     {
      $GPSInfo = $False
     }
                
    If ($GPSInfo -eq $true)
     {
      $LonEW = $Encode.GetString($img.GetPropertyItem(3).Value)
      
      [Double]$LatDeg, [Double]$LatMin, [Double]$LatSec = Get-GPSDetails -FileNameWithGPSDetails $FileName -ID 2 
      [Double]$LonDeg, [Double]$LonMin, [Double]$LonSec = Get-GPSDetails -FileNameWithGPSDetails $FileName -ID 4

      $GPSLat = "$([int]$LatDeg)º $([int]$LatMin)' $($([double]$LatSec).ToString("##.######"))$([char]34) $LatNS"
      $GPSLon = "$([int]$LonDeg)º $([int]$LonMin)' $($([double]$LonSec).ToString("##.######"))$([char]34) $LonEW"

      Add-EntryToLogFile -Entry "    The picture $FileName has the following GPS coordinates:"
      Add-EntryToLogFile -Entry "       $GPSLat"        
      Add-EntryToLogFile -Entry "       $GPSLon"

    # =============================================================================================================================================
    # Convert the latitude and longitude to numbers that Google Maps recognizes.
    # =============================================================================================================================================

      $LatOrt = 0
      $LonOrt = 0

      If ($LatNS -eq 'S')
       {
        $LatOrt = "-"   
       }
      If ($LonEW -eq 'W')
       {
        $LonOrt = "-"
       }

      $LatDec = ($LatDeg + ($LatMin/60) + ($LatSec/3600))
      $LonDec = ($LonDeg + ($LonMin/60) + ($LonSec/3600))

      $LatOrt = $LatOrt + $LatDec
      $LonOrt = $LonOrt + $LonDec

    # =============================================================================================================================================
    # The numbers that where returned contained a decimal comma instead of a decimal point. 
    # So the en-US culture is forced to get the correct number notation.
    # =============================================================================================================================================
    
      $LatOrt = $LatOrt.ToString([cultureinfo]::GetCultureInfo('en-US'))
      $LonOrt = $LonOrt.ToString([cultureinfo]::GetCultureInfo('en-US'))

      Add-EntryToLogFile -Entry "    The picture $FileName has the following decimal coordinates:"
      Add-EntryToLogFile -Entry "      $LatOrt"
      Add-EntryToLogFile -Entry "      $LonOrt"
    }
     else
    {
     
   # =============================================================================================================================================
   # Ohoh... No GPS information in this picture.
   # =============================================================================================================================================
     
     Add-EntryToLogFile -Entry "    The picture $FileName does not contain GPS information."
     $LatOrt = "1024"
     $LonOrt = "1024"
    }

    Return $LatOrt,$LonOrt,$GPSLat,$GPSLon
  }

  Function Get-AddressBasedOnLatitudeAndLongitude
   {

    <#
    .NOTES
    =============================================================================================================================================
    Created with:     Windows PowerShell ISE
    Created on:       03-August-2018
    Created by:       Willem-Jan Vroom
    Organization:     
    Functionname:     Get-AddressBasedOnLatitudeAndLongitude
    =============================================================================================================================================
    .SYNOPSIS

    This function finds the address based on latitude and longitude.
    It is also called reversed geocoding.

    #>

    Param
     (
      [String] $Latitude,
      [string] $Longitude
     )

     $Result  = Invoke-WebRequest "https://maps.googleapis.com/maps/api/geocode/json?latlng=$Latitude,$Longitude&key=$Global:GoogleAPIKey" | ConvertFrom-Json

     if($Result.status -eq "OK")
      {
       $MostAccurateAddress = $Result.results.formatted_address[0]
       Add-EntryToLogFile -Entry "    The Google API Key has been enabled for reversed geocoding."
       Add-EntryToLogFile -Entry "    Found address: $MostAccurateAddress"
       Return $MostAccurateAddress
      }
       elseif($Result.status -eq "REQUEST_DENIED")
      {
       Add-EntryToLogFile -Entry "    The given Google API Key has not been enabled for reversed geocoding."
       Add-EntryToLogFile -Entry "    Please perfrom the following steps to enable reversed geocoding:"
       Add-EntryToLogFile -Entry "      1. Go to https://console.cloud.google.com/apis/dashboard"
       Add-EntryToLogFile -Entry "      2. Click on ENABLE APIS AND SERVICES"
       Add-EntryToLogFile -Entry "      3. In the 'Search for APIs & Services' enter 'Geocoding API'."
       Add-EntryToLogFile -Entry "      4. Click on Geocoding API"
       Add-EntryToLogFile -Entry "      5. Click on the [Enable] button."
      }
       elseif($Result.status -eq "ZERO_RESULTS")
      {
       $Message = "Reverse geocoding was successful but returned no results."
       Add-EntryToLogFile -Entry $Message
       Return $Message
      }

      
      Add-EntryToLogFile -Entry "Reversed geocoding came back with the error: '$($Result.Status)'."

      Return ""
  }

# =============================================================================================================================================
# End function block
# =============================================================================================================================================

# =============================================================================================================================================
# Declares the variables.
# =============================================================================================================================================

  Clear-host
  
  $Global:arrHTMLTable    = @()
  $Global:DetailedLogging = $DetailedLogging
  $Global:GoogleAPIKey    = $GoogleAPIKey
  $Global:LogFile         = ""
  $Message                = ""

  
  if($LogPath -eq "")
   {
    $LogPath   = Split-Path -parent $MyInvocation.MyCommand.Definition
   }
    else
   {
    $Returntext, $bolResult = Create-Folder -FolderName $LogPath
    if($bolResult)
     {  
      $Message = $Returntext
     }
      else
     {
      Write-Error $Returntext -Category WriteError
      Exit 2
     }
   }

# =============================================================================================================================================
# Add assemblies.
# =============================================================================================================================================

  Add-Type -AssemblyName System.Web
  Add-Type -AssemblyName System.Drawing

# =============================================================================================================================================
# Define the HTML File.
# =============================================================================================================================================
 
  $LastPartOfHTMLFile = " (" + (Get-Date).ToString('G') + ").html"
  $LastPartOfHTMLFile = $LastPartOfHTMLFile -replace ":","-"
  $LastPartOfHTMLFile = $LastPartOfHTMLFile -replace "/","-"
  $PreFixLogFile      = "Pictures"
  $PrefixCSS          = "Picture"

  $HTMLFile   = $LogPath + "\"+ $PreFixLogFile + $LastPartOfHTMLFile

# =============================================================================================================================================
# Define the log File.
# =============================================================================================================================================

  if($Global:DetailedLogging)
   {
    $Global:LogFile = $LogPath + "\"+ $PreFixLogFile + $($LastPartOfHTMLFile -replace ".html",".log")
    New-Item $Global:LogFile -ItemType File -Force | Out-Null
   }

# =============================================================================================================================================
# Find all the arguments and put them in the log file
# Source: https://ss64.com/ps/psboundparameters.html
# =============================================================================================================================================

  Add-EntryToLogFile -Entry  "---------------------------------------------------------------------------------------------------------------"
  Add-EntryToLogFile -Entry  "Used parameters:" 
  ForEach($boundparam in $PSBoundParameters.GetEnumerator()) 
   {
    Add-EntryToLogFile -Entry " * Key: $($boundparam.Key) Value: $($boundparam.Value)" 
   }
  Add-EntryToLogFile -Entry  "---------------------------------------------------------------------------------------------------------------"

# =============================================================================================================================================
# Check if there is a working internet connection. It will check if Google can be reached.
# =============================================================================================================================================

  $InternetConnection     = WorkingInternetConnection

# =============================================================================================================================================
# Define header and the various files that are used within the website. 
# =============================================================================================================================================

  $Header      = ""
  $VarOptions  = "" 
  $VarMap      = ""
  $Marker      = ""
  If($GoogleAPIKey -and $InternetConnection)
   {
    $Header     += "  <script src=$([char]34)https://polyfill.io/v3/polyfill.min.js?features=default$([char]34)></script>`n"
    $Header     += "     <script`n"
    $Header     += "        src="/joomla/+ $([char]34) +"https://maps.googleapis.com/maps/api/js?key=$($GoogleAPIKey)&callback=initialize&libraries=&v=weekly$([char]34)`n"
    $Header     += "        defer`n"
    $Header     += "   ></script>`n"
    $Header     += "  <script>`n"
    $Header     += "  function initialize() {`n"
   }

  $CSS         = "<style type=$([char]34)text/css$([char]34)>`n"
  $CSS        += "table  {`n"
  $CSS        += "        font-size:       12px`n"
  $CSS        += "        border:          0px;`n"
  $CSS        += "        font-family:     Arial, Helvetica, sans-serif;`n"
  $CSS        += "       }`n`n"
  $CSS        += "td     {`n"
  $CSS        += "        padding:        4px;`n"
  $CSS        += "        margin:         0px;`n"
  $CSS        += "        border:         1px;`n"
  $CSS        += "        width:          400px;`n"
  $CSS        += "        vertical-align: top;`n"
  $CSS        += "       }`n`n"
  $CSS        += "th     {`n"
  $CSS        += "        background:     #395870;`n"
  $CSS        += "        background:     linear-gradient(#49708f, #293f50);`n"
  $CSS        += "        color:          #fff;`n"
  $CSS        += "        font-size:      11px;`n"
  $CSS        += "        padding:        10px 15px;`n"
  $CSS        += "        vertical-align: top;`n"
  $CSS        += "       }`n`n"
  $CSS        += "tr     {`n"
  $CSS        += "        width:          1000px;`n"
  $CSS        += "       }`n`n"
  $CSS        += "h1     {`n"
  $CSS        += "        font-family:    Arial, Helvetica, sans-serif;`n"
  $CSS        += "        color:          #e68a00;`n"
  $CSS        += "        font-size:      28px;`n"
  $CSS        += "        text-align:     center;`n"
  $CSS        += "       }`n`n"
  $CSS        += "h2     {`n"
  $CSS        += "        font-family:    Arial, Helvetica, sans-serif;`n"
  $CSS        += "        color:          #000099;`n"
  $CSS        += "        font-size:      16px;`n"
  $CSS        += "        text-align:     center;`n"
  $CSS        += "       }`n`n"
  $CSS        += "#Cred  {`n"
  $CSS        += "        font-family:    Arial, Helvetica, sans-serif;`n"
  $CSS        += "        color:          #0000ff;`n"
  $CSS        += "        font-size:      12px;`n"
  $CSS        += "        text-align:     left;`n"
  $CSS        += "       }`n`n"
  $CSS        += "#Alert {`n"
  $CSS        += "        font-family:    Arial, Helvetica, sans-serif;`n"
  $CSS        += "        color:          red;`n"
  $CSS        += "        font-size:      14px;`n"
  $CSS        += "        text-align:     left;`n"
  $CSS        += "       }`n`n"


# =============================================================================================================================================
# Define the directory where the photo's are located.
# =============================================================================================================================================  
  
  if(-not $Directory)
   {
    $Directory     = Split-Path -parent $MyInvocation.MyCommand.Definition
   }
   
# =============================================================================================================================================
# Read the directory with all the file names with the *.jpg or *.jpeg extension.
# The -filter option can only contain one extension. 
# =============================================================================================================================================

  $JPGFileNames  = @(Get-ChildItem -Path $Directory -Filter *.jpg  -Recurse:$IncludeSubFolders)
  $JPGFileNames += @(Get-ChildItem -Path $Directory -Filter *.jpeg -Recurse:$IncludeSubFolders)

  $JPGFileNames  = $JPGFileNames | Sort-Object

  if($ShowOnlyUniquePictures)
   {
    $JPGFileNames = $JPGFileNames | Get-Unique
   }
  
# =============================================================================================================================================
# Go through all the jpg files.
# =============================================================================================================================================

  $TotalJPGFiles = $JPGFileNames.Count
  $valCounter    = 1

  ForEach ($JPGFileName in $JPGFileNames)
   {
    Write-Progress -Id 1 -Activity "Processing all the jpg files." -Status "Processing file $JPGFileName   (Number $valCounter of $TotalJPGFiles)" -PercentComplete ($valCounter / $TotalJPGFiles * 100)

    Add-EntryToLogFile -Entry "Processing picture: $($JPGFileName.FullName)"  
    
    $tmpPhotoData = (Get-PhotoMetaDataFromJPGFile -FileName $($JPGFileName.FullName))
    $tmpPicture   = "<img src=$([char]34)$($JPGFileName.FullName)$([char]34) style=$([char]34)width:600px;$([char]34)>`n"
    $tmpString    = "00000000$valCounter"
    $Picture      = "$PrefixCSS$($tmpString.Substring($tmpString.Length-8))"
    
    Add-EntryToLogFile -Entry "    Image source: $tmpPicture"
    if($InternetConnection)
     {
      If($GoogleAPIKey)
       {
        $Latitude,$Longitude,$GPSLatitude,$GPSLongitude = Get-EXIFDataFromJPGFile -FileName $($JPGFileName.FullName)
        if(-not($Latitude -eq "1024" -and $Longitude -eq "1024"))
         {
          Add-EntryToLogFile -Entry "    A valid latitude and longitude have been found in this picture."
          $Header     += " var my$Picture = new google.maps.LatLng(" + $Latitude + "," + $Longitude + ");`n"
          $Address     = Get-AddressBasedOnLatitudeAndLongitude -Latitude $Latitude -Longitude $Longitude 

          $VarOptions += " var Options$Picture = {`n"
          $VarOptions += " zoom:           17,`n"
          $VarOptions += " center:         my$Picture,`n"
          $VarOptions += " mapTypeId:      google.maps.MapTypeId.ROADMAP,`n"
          $VarOptions += " mapTypeControl: 0`n"
          $VarOptions += "}`n"

          $VarMap     += " var map$Picture = new google.maps.Map(document.getElementById('map-$Picture'), Options$Picture);`n"

          $Marker     += " var Marker$Picture = new google.maps.Marker({`n"
          $Marker     += " position: my$Picture,`n"
          $Marker     += " map:      map$Picture,`n"
          $Marker     += " title:    '$Picture'`n"
          $Marker     += "});`n"

          $CSS        += " #map-$Picture {`n"
          $CSS        += "                            width:            400px;`n"
          $CSS        += "                            height:           400px;`n"
          $CSS        += "                            background-color: #CCC;`n"
          $CSS        += "                           }`n`n"

          $tmpGoogleMapsMap  = "<div id=$([char]34)map-$Picture$([char]34)"
          $tmpGoogleMapsMap += "></div>`n"
          $tmpGoogleMapsMap += "<p>Address: $Address</p>"
          $tmpGoogleMapsMap += "<p>GPS Coordinates:<br>"
          $tmpGoogleMapsMap += "  $GPSLatitude<br>"
          $tmpGoogleMapsMap += "  $GPSLongitude</p>"
         }
         else
         {
          $ErrorMessage = "There are no coordinates found in the picture."
          Add-EntryToLogFile -Entry "    * Error: $ErrorMessage"
          $tmpGoogleMapsMap = $ErrorMessage
         }
       }
       else
       {
         Add-EntryToLogFile -Entry "   * Error: No Google Maps API Provided. Specify one as a parameter or register one via https://developers.google.com/maps/documentation/javascript/get-api-key and use it as a parameter"
         $tmpGoogleMapsMap  = "There is no Google Maps API provided.<br>"
         $tmpGoogleMapsMap += "Now, there are two options:<br>"
         $tmpGoogleMapsMap += "<ol>"
         $tmpGoogleMapsMap += "  <li>Use the parameter <b>GoogleAPIKey</b> and provide the APIKey.</li>"
         $tmpGoogleMapsMap += "  <li>Register a new Google API Key via <a href=$([char]34)https://developers.google.com/maps/documentation/javascript/get-api-key$([char]34)>Get an API Key on Google Maps Platform</a>.</li>"
         $tmpGoogleMapsMap += "</ol>"
       }    
     }
      else
     {
      $tmpGoogleMapsMap  = "There is no working internet connection. So no Google Maps information.<br>"
     }
    
    Add-EntryToHTMLTable -Picture $tmpPicture -PhotoData $tmpPhotoData -GoogleMapsMap $tmpGoogleMapsMap
    $valCounter++
   }

# =============================================================================================================================================
# Format the HTML Header
# =============================================================================================================================================

  $Header  = $Header + "`n" + $VarOptions + "`n"+ $VarMap + "`n"+ $Marker
  If($GoogleAPIKey -and $InternetConnection)
   {
    $Header += "}`n"
    $Header += "</script>`n`n"
   }
  $Header += $CSS
  $Header += "</style>"
  $Header += ""

  Add-EntryToLogFile -Entry "   The header: `n$Header"

# =============================================================================================================================================
# It appeared that the table layout was scrambled with - for example -
# <td>&lt;img src=&quot;.\0.jpg&quot; style=&quot;width:600px;&quot;&gt;</td>
# Add-Type -AssemblyName System.Web and
# [System.Web.HttpUtility]::HtmlDecode($HTMLTable)
# solved this issue.
# Source: https://stackoverflow.com/questions/23143393/powershell-convertto-html-is-translating-and-symbols-to-lt-and-gt-how
#
# Create the html table
# =============================================================================================================================================

  $HTMLTable = $Global:arrHTMLTable | ConvertTo-Html -Property Picture,"Photo Data","Google Maps Map" -Fragment -PreContent "<h2>The pictures</h2>"
  $HTMLTable = [System.Web.HttpUtility]::HtmlDecode($HTMLTable)

# =============================================================================================================================================
# Create the whole page.
# =============================================================================================================================================

  $Title       = "<h1>Complete overview of the pictures used</h1>"

  $Timestamp   = (Get-Date -UFormat "%a %e %b %Y %X").ToString()
  $PostContent = "<p id=$([char]34)Cred$([char]34)>Creation Date: $Timestamp</p>"
  $Report      = ConvertTo-Html -Head $Header -Body "$Title $HTMLTable" -PostContent $PostContent 
  $Report | Out-File $HTMLFile

And instructions on how to use this script:

And a progress bar is shown:

For this article I used the following links (in random order):

 

Add comment