Reading coordinates from the photo in .NET Core

Accessing GPS coordinates metadata of a photography using C# and .NET Core

Pretty much all models of digital cameras including smartphones have ability and usually by default store additional metadata along with the image itself taken. This data is stored in EXIF format and you can easily see it if you are a Windows user by checking the properties of the photography file.

Photo Exif 

This is one of the ways social networks know where you took your photos becaus all this extra daat is supplied with the photo when you upload your file. EXIF metadata description and fields can be found in Metadata Refrence Table - Standard Exif Tags. Since we are going to focus only on GPS data collected when the photo is taken the following are two metadata keys we are going to focus on. 

Tag (hex)Tag (dec)IFDKeyTypeTag description
0x00022GPSInfoExif.GPSInfo.GPSLatitudeRationalIndicates the latitude. The latitude is expressed as three RATIONAL values giving the degrees, minutes, and seconds, respectively. When degrees, minutes and seconds are expressed, the format is dd/1,mm/1,ss/1. When degrees and minutes are used and, for example, fractions of minutes are given up to two decimal places, the format is dd/1,mmmm/100,0/1.
0x00044GPSInfoExif.GPSInfo.GPSLongitudeRationalIndicates the longitude. The longitude is expressed as three RATIONAL values giving the degrees, minutes, and seconds, respectively. When degrees, minutes and seconds are expressed, the format is ddd/1,mm/1,ss/1. When degrees and minutes are used and, for example, fractions of minutes are given up to two decimal places, the format is ddd/1,mmmm/100,0/1.

Initially, .NET Core did not have support for Bitmap classes, so to read EXIF metadata, you would have to use 3rd party nuget packages. SInce there wera a lot of complains and it was hard to migrate large number of applications from .NET 4.x to .NET Core, support for Bitmap is added in a form of NuGet package System.Drawing.Common. So in order to work with Bitmap, first step is to install this NuGet package.

You can do it from .NET CLI directly

> dotnet add package System.Drawing.Common --version 4.5.1

Or if you are in Visual Studio you can do it from the Package Manager Console

PM> Install-Package System.Drawing.Common -Version 4.5.1

Byt the most convenient way for me to install packages to my project is to do it directly in the project file. By default, Visual Studio open .NET Core projects in a text editor by simple double click in the Solution Explorer pane of Visual Studio IDE

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Drawing.Common" Version="4.5.1" />
  </ItemGroup>

</Project>

    
Note

I am currently using .NET Core 3.0 preview for my project which should be officially released in Q1 2019. To be able to play with .NET Core before the official release, you also need to have Visual Studio 2019 preview. If you are not into the new releases, feel free to downgrade your project TargetFramework to netcoreapp2.2. More details on .NET Core releases can be find on the roadmap page https://github.com/dotnet/core/blob/master/roadmap.md

To do the demo I found this photo which contains a lot of metadata, but as I mentioned we'll focus on GPS data because it is a bit tricky to interpret it to be used for example to get location shown on Google Maps.

Content Example Ibiza

So let's start with accessing the metadata of the Bitmap instance. All metadata is organized in Bitmap.PropertyItems property which is basically an array containing instances of PropertyItem class. If you check the structure of PropertyItem class you will see that it has it's identifier (Id) and the data (Value).

#region Assembly System.Drawing.Common, Version=4.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
// C:\Users\Dejan_2\.nuget\packages\system.drawing.common\4.5.1\ref\netstandard2.0\System.Drawing.Common.dll
#endregion

namespace System.Drawing.Imaging
{
    public sealed class PropertyItem
    {
        public int Id { get; set; }
        public int Len { get; set; }
        public short Type { get; set; }
        public byte[] Value { get; set; }
    }
}
    

We'll focus only on Id and Value properties. Id value matches to the EXIF Metadata Reference Table, so for GPS data, specifically for Longitude and Latitude we need PropertyItem instances with Id values of 2 (Latitude) and 4 (longitude). To do this we can just quicly filter the PropertyItems array with LINQ.

            using (Bitmap bitmap = new Bitmap(@"D:\tmp\content_example_ibiza.jpg"))
            {
                var longitudeData = bitmap.PropertyItems.Single(p => p.Id == 4).Value;
                var latitudeData = bitmap.PropertyItems.Single(p => p.Id == 2).Value;
            }
    

Cool we got the data, but this was the easier part of the task. The data returned is byte array (byte[]). As this data is useless to use it with GoogleMaps. We need to convert these values to Double in order to be able to pass it to GoogleMaps as URL parameters and as much as it looks simple initially, it is not that easy.

If you refer to metadata table, both longitude and latitude are represented as RATIONAL data values which is equivalent to PropertyTagTypeRational. After some time spent on Google, finally found the way to decrypt the byte array contained in Value property

uint degreesNumerator   = BitConverter.ToUInt32(propItem.Value, 0);
uint degreesDenominator = BitConverter.ToUInt32(propItem.Value, 4);
uint minutesNumerator   = BitConverter.ToUInt32(propItem.Value, 8);
uint minutesDenominator = BitConverter.ToUInt32(propItem.Value, 12);
uint secondsNumerator   = BitConverter.ToUInt32(propItem.Value, 16);
uint secondsDenominator = BitConverter.ToUInt32(propItem.Value, 20);
    

Now, we read these values from the byte array but it is still not useful to the level we can use it with GoogleMaps as Google takes Longitude and Latitude as real number values. Aftre another session of searching and collecting bits and pieces, I got to this method for transforming this byte arrays to the real number values.

        private static double GetCoordinateDouble(PropertyItem propItem)
        {
            uint degreesNumerator = BitConverter.ToUInt32(propItem.Value, 0);
            uint degreesDenominator = BitConverter.ToUInt32(propItem.Value, 4);
            double degrees = degreesNumerator / (double)degreesDenominator;


            uint minutesNumerator = BitConverter.ToUInt32(propItem.Value, 8);
            uint minutesDenominator = BitConverter.ToUInt32(propItem.Value, 12);
            double minutes = minutesNumerator / (double)minutesDenominator;

            uint secondsNumerator = BitConverter.ToUInt32(propItem.Value, 16);
            uint secondsDenominator = BitConverter.ToUInt32(propItem.Value, 20);
            double seconds = secondsNumerator / (double)secondsDenominator;

            double coorditate = degrees + (minutes / 60d) + (seconds / 3600d);
            string gpsRef = System.Text.Encoding.ASCII.GetString(new byte[1] { propItem.Value[0] }); //N, S, E, or W

            if (gpsRef == "S" || gpsRef == "W")
            {
                coorditate = coorditate * -1;
            }
            return coorditate;
        }
    

Now the whole problem solution is there, all that is left is to construct GoogleMaps URL for the values pulled out from image EXIF metadata. 

    class Program
    {
        static void Main(string[] args)
        {
            using (Bitmap bitmap = new Bitmap(@"D:\tmp\content_example_ibiza.jpg"))
            {
                var longitude = GetCoordinateDouble(bitmap.PropertyItems.Single(p => p.Id == 4));
                var latitude = GetCoordinateDouble(bitmap.PropertyItems.Single(p => p.Id == 2));

                Console.WriteLine($"Longitude: {longitude}");
                Console.WriteLine($"Latitude: {latitude}");

                Console.WriteLine($"https://www.google.com/maps/place/{latitude},{longitude}");

            }

            Console.ReadKey();

        }


        private static double GetCoordinateDouble(PropertyItem propItem)
        {
            uint degreesNumerator = BitConverter.ToUInt32(propItem.Value, 0);
            uint degreesDenominator = BitConverter.ToUInt32(propItem.Value, 4);
            double degrees = degreesNumerator / (double)degreesDenominator;


            uint minutesNumerator = BitConverter.ToUInt32(propItem.Value, 8);
            uint minutesDenominator = BitConverter.ToUInt32(propItem.Value, 12);
            double minutes = minutesNumerator / (double)minutesDenominator;

            uint secondsNumerator = BitConverter.ToUInt32(propItem.Value, 16);
            uint secondsDenominator = BitConverter.ToUInt32(propItem.Value, 20);
            double seconds = secondsNumerator / (double)secondsDenominator;

            double coorditate = degrees + (minutes / 60d) + (seconds / 3600d);
            string gpsRef = System.Text.Encoding.ASCII.GetString(new byte[1] { propItem.Value[0] }); //N, S, E, or W

            if (gpsRef == "S" || gpsRef == "W")
            {
                coorditate = coorditate * -1;
            }
            return coorditate;
        }

    }
    

 And the output of this simple console application is:

Longitude: 1.43866666666667
Latitude: 38.9098333333333
https://www.google.com/maps/place/38.9098333333333,1.43866666666667

To make sure we did not do anything wrong, let's open the URL in the browser.

 Map

As you can see, map points to a location in Spain, Ibiza, which is where the photo is taken.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform includion ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article