.netimageexifgeotagging

How can I add EXIF information to geotag an Image in .NET?


I have an Image in .NET. How can I geotag the image by encoding the latitude and longitude at which it was produced using EXIF data, but without needing to use an external library?


Solution

  • EXIF information can be attached to Images in .NET 2.0 and higher using PropertyItems, which translate to individual EXIF fields. The details of these fields may be found in the EXIF 2.3 standard, but we only need five of them to geotag an image. The sample C# code below requires references to System.Drawing, System.Drawing.Imaging, and System.IO. To test it, simply use the line below. You can verify that the image has been correctly geotagged by examining it with this tool (or one of many others).

    Geotag(new Bitmap(@"C:\path\to\image.jpg"), 34, -118)
        .Save(@"C:\path\to\geotagged.jpg", ImageFormat.Jpeg);
    

    One thing that may look odd about the code below is that a PropertyItem is reused to create a new PropertyItem. It is not obvious that this will work since mutating the existing PropertyItem (which is a class rather than a struct) seems like it would affect the existing property. However, this turns out not to be the case and this hack is necessary because there is no public constructor for PropertyItem.

    static Image Geotag(Image original, double lat, double lng)
    {
        // These constants come from the CIPA DC-008 standard for EXIF 2.3
        const short ExifTypeByte = 1;
        const short ExifTypeAscii = 2;
        const short ExifTypeRational = 5;
    
        const int ExifTagGPSVersionID = 0x0000;
        const int ExifTagGPSLatitudeRef = 0x0001;
        const int ExifTagGPSLatitude = 0x0002;
        const int ExifTagGPSLongitudeRef = 0x0003;
        const int ExifTagGPSLongitude = 0x0004;
    
        char latHemisphere = 'N';
        if (lat < 0)
        {
            latHemisphere = 'S';
            lat = -lat;
        }
        char lngHemisphere = 'E';
        if (lng < 0)
        {
            lngHemisphere = 'W';
            lng = -lng;
        }
    
        MemoryStream ms = new MemoryStream();
        original.Save(ms, ImageFormat.Jpeg);
        ms.Seek(0, SeekOrigin.Begin);
    
        Image img = Image.FromStream(ms);
        AddProperty(img, ExifTagGPSVersionID, ExifTypeByte, new byte[] { 2, 3, 0, 0 });
        AddProperty(img, ExifTagGPSLatitudeRef, ExifTypeAscii, new byte[] { (byte)latHemisphere, 0 });
        AddProperty(img, ExifTagGPSLatitude, ExifTypeRational, ConvertToRationalTriplet(lat));
        AddProperty(img, ExifTagGPSLongitudeRef, ExifTypeAscii, new byte[] { (byte)lngHemisphere, 0 });
        AddProperty(img, ExifTagGPSLongitude, ExifTypeRational, ConvertToRationalTriplet(lng));
    
        return img;
    }
    
    static byte[] ConvertToRationalTriplet(double value)
    {
        int degrees = (int)Math.Floor(value);
        value = (value - degrees) * 60;
        int minutes = (int)Math.Floor(value);
        value = (value - minutes) * 60 * 100;
        int seconds = (int)Math.Round(value);
        byte[] bytes = new byte[3 * 2 * 4]; // Degrees, minutes, and seconds, each with a numerator and a denominator, each composed of 4 bytes
        int i = 0;
        Array.Copy(BitConverter.GetBytes(degrees), 0, bytes, i, 4); i += 4;
        Array.Copy(BitConverter.GetBytes(1), 0, bytes, i, 4); i += 4;
        Array.Copy(BitConverter.GetBytes(minutes), 0, bytes, i, 4); i += 4;
        Array.Copy(BitConverter.GetBytes(1), 0, bytes, i, 4); i += 4;
        Array.Copy(BitConverter.GetBytes(seconds), 0, bytes, i, 4); i += 4;
        Array.Copy(BitConverter.GetBytes(100), 0, bytes, i, 4);
        return bytes;
    }
    
    static void AddProperty(Image img, int id, short type, byte[] value)
    {
        PropertyItem pi = img.PropertyItems[0];
        pi.Id = id;
        pi.Type = type;
        pi.Len = value.Length;
        pi.Value = value;
        img.SetPropertyItem(pi);
    }