Working with EXIF ​​geotags in C #

After I finished the geotagging program, I got the idea to write this article - so that fewer people stepped on the same rake, since there is not so much sensible information on these issues.
So, I'm not going to tell what geotagging or EXIF is , you can read about it on Wikipedia. But I’m going to tell how to make a program in C # that would read and write data in EXIF. We will work with photo metadata, as, in my opinion, the simplest method is through JpegBitmapDecoder , for this you will need to connect several modules
using System.IO;
using System.Globalization;
using System.Windows.Media.Imaging;

* This source code was highlighted with Source Code Highlighter.

To get started, simply open the photo file:
FileStream Foto = File.Open(s, FileMode.Open, FileAccess.Read); // открыли файл по адресу s для чтения
          BitmapDecoder decoder = JpegBitmapDecoder.Create(Foto, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); //"распаковали" снимок и создали объект decoder
          BitmapMetadata TmpImgEXIF = (BitmapMetadata)decoder.Frames[0].Metadata.Clone(); //считали и сохранили метаданные

* This source code was highlighted with Source Code Highlighter.

Very often you need to get the date and time of the picture. If you look at the EXIF ​​specification , then there are several dates stored there (date and time of taking the picture, creating a file, digitizing, GPS, etc.), and it is still not clear which one the camera recorded and which one it ignored. But do not really bother with this - everything is simple here. BitmapMetadata
contains properties for obtaining the date and time of shooting, as well as some other parameters (for example, camera model).
DateTime DateOfShot = Convert.ToDateTime(TmpImgEXIF.DateTaken);

* This source code was highlighted with Source Code Highlighter.

Now let's go directly to the geotag record. To work with any metadata (not just EXIF), BitmapMetadata contains the SetQuery and GetQuery methods . They take a query string as a parameter, which determines which metadata field to read or write. There is also RemoveQuery for deleting a field. Let's start with a simple one: add a mark in EXIF ​​that we have north latitude. In EXIF, it corresponds to the GPS section field with ID = 1, and in C #, the request is "/ app1 / ifd / gps / {ushort = 1}" (where will I get these requests from later) and the data type is string. If the north latitude is written “N”, and the south - “S”:
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=1}", "N");

* This source code was highlighted with Source Code Highlighter.

Similarly with longitude:
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=3}", "E");

* This source code was highlighted with Source Code Highlighter.

And the version, it should be 2.2.0.0:
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=0}", "2.2.0.0");

* This source code was highlighted with Source Code Highlighter.

Now it’s more complicated: add altitude - this is an optional parameter, but on its example we will consider working with the Rational type . If you open the EXIF ​​specification , then you can see that the height above sea level is stored in the Rational format. This type expresses real numbers as a simple fraction, so the number 182.053 will be written as 182053/1000 (or 1820530/10000). To work with this type in C #, you can select the ulong type and use 4 low bytes to store the numerator, and 4 high bytes for the denominator. Here is a function for converting double to Rational:
private ulong rational(double a)
    {
      uint denom = 1000;
      uint num = (uint)(a * denom);
      ulong tmp;
      tmp = (ulong)denom << 32;
      tmp |= (ulong)num;
      return tmp;
    }

* This source code was highlighted with Source Code Highlighter.

Let's record the height of 95.3m (in EXIF, the height above sea level is stored in meters, for example, in contrast to the .plt track where it is stored in feet).
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=2}", rational(95.3));

* This source code was highlighted with Source Code Highlighter.

Well, now add the latitude and longitude. It is stored in the form of three Rational, which express degrees, minutes, seconds and fractions of a second. Degrees and minutes are stored in integers (that is, the denominator is 1, but this is not a prerequisite), and seconds are real. Example: 50⁰30'12.345 ″. In C #, these three numbers must be combined into an array. Here is an example of recording latitude:
ulong[] t = { rational(50), rational(30), rational(12.345) };
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=2}", t);

* This source code was highlighted with Source Code Highlighter.

and longitudes:
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=4}", t);

* This source code was highlighted with Source Code Highlighter.

Well, that’s probably all. Sometimes it is necessary to translate the coordinates from the format of degrees and fractions of a degree obtained from a track file, a tag, any Internet service and others, since such a format is more convenient for work, but I think that there should not be any problems with such a conversion. Just in case, here are the translation formulas:
Degree = Math.Floor(value);
Minute = Math.Floor(((value - Math.Floor(value)) * 60.0));
Second = (((value - Math.Floor(value)) * 60.0) - Math.Floor(((value - Math.Floor(value)) * 60.0))) * 60;

* This source code was highlighted with Source Code Highlighter.

So, we have a BitmapMetadata object with new entries added (if something was already written down before us, then it should automatically be replaced with new values). Now create a new snapshot file, into which we transfer everything except the metadata from the first file, and take the metadata that we changed.
JpegBitmapEncoder Encoder = new JpegBitmapEncoder();//создали новый энкодер для Jpeg
          Encoder.Frames.Add(BitmapFrame.Create(decoder.Frames[0], decoder.Frames[0].Thumbnail, TmpImgEXIF, decoder.Frames[0].ColorContexts)); //добавили в энкодер новый кадр(он там всего один) с указанными параметрами
          string NewFileName = s + "+GeoTag.jpg";//имя исходного файла +GeoTag.jpg
          using (Stream jpegStreamOut = File.Open(NewFileName, FileMode.CreateNew, FileAccess.ReadWrite))//создали новый файл
          {
            Encoder.Save(jpegStreamOut);//сохранили новый файл
          }
          Foto.Close();//и закрыли исходный файл

* This source code was highlighted with Source Code Highlighter.

Now we have a new jpeg file, but with a geotag. Please note that the size of the source file and the new one can vary significantly, this is due to different ways and parameters of image compression. You can work with any EXIF ​​fields (tags) using the SetQuery and GetQuery methods. GetQuery works the same way - it accepts a query string, returns a value of what type - we look in the EXIF ​​specification . What request, what parameter answers you can see here . For example, the date read function:
GetQuery("/app1/ifd/exif:{uint=36867}");

* This source code was highlighted with Source Code Highlighter.

You can use your queries as shown in this example .

Also popular now: