Click here to Skip to main content
15,886,101 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
The older methods for modifying EXIF metadata appear to use the System.Drawing.Image namespace -- and the propertyItems property of this class. I have seen some indication that this approach only allows modifying existing EXIF data and not adding new metadata where none existed before. In my case, I'd like to add GPS tags to a jpg image that previously had none.

Can anyone verify that new propertyItems cannot be added to an existing image -- thus extending the length of the metadata header?

The newer approach seems to be to use the System.Windows.Media.Imaging namespace and its InPlaceBitmapMetadataWriter method.

Does anyone have an example of doing this? The example would begin with an image w/o the GPS tags and end up with an image that has the GPS tags.

--- Jim
Posted
Comments
Member 10020245 8-Sep-14 9:55am    
The article here:
https://searchcode.com/codesearch/view/8739374/
provides a good illustration of using the InPlaceBitmapMetadataWriter to add new tags to pre-existing metadata from a jpg image.

The example uses the metadata class SetQuery(string query, Object value) command. The exact syntax used in the example is
metadata.SetQuery ( "/app1/ifd/{uint=897}", "hello there" );
I am not clear on the meaning of {uint=897}. I assume this defines the tag location that carries a definition of the type of metadata. In this case it would be an ASCII value.
Can someone show how this command would be modified if I want to add a GPS latitude tag? I am aware that the GPS latitude (and longitude) are comprised of 3 exif 8-byte Rational types or a byte array of 24 bytes.

http://blogs.msdn.com/b/kamalds/archive/2012/04/08/adding-updating-exif-data.aspx[^] has an example of how to create EXIF fields if they don't exist on the image yet.

Basically, you need to know the EXIF field code for the field, and follow the code in the WriteEXIFField() method in the sample code to create the new field.

UPDATE: You can find a list of EXIF tags here: http://www.exiv2.org/tags.html[^]

And you can find information on how to create and parse the Rational type that the Latitude and Longitude are stored as in the source code of this project in the Util class: ExifTagCollection - An EXIF metadata extraction library[^]
 
Share this answer
 
v2
Comments
Member 10020245 4-Sep-14 16:21pm    
Thanks --- will try that. I was hoping I could get around reading in the whole image, copying it, then writing it out again. But maybe that's required cause the metadata is in the front.
Member 10020245 4-Sep-14 16:35pm    
In the above Solution 1, there is a reference to "Kevin's work":
This blog gives an idea of adding/updating EXIF metadata of an image, and is based on the Kevin's work. The original post can be found here.

This link is broken. Do you have an updated link?
Else is the posed solution a standalone solution -- or do I need material from the link?
kbrandwijk 4-Sep-14 18:50pm    
I don't think you need any material from the original link. Have you actually tried the method specified. Have you looked up the correct EXIF field identifiers for the information that you are trying to add? Have you actually tried to use the WriteEXIFField() method to add an EXIF field to one of your existing images? If you need additional help, at least share with us what code you currently have, and if there are any concrete issues you might still have. The solution I linked to, and the method I point to, contains all the implementation details you need.
kbrandwijk 4-Sep-14 19:08pm    
I have updated my solution to include information about the EXIF fields and the way to store the GPS information in the right format.
Member 10020245 5-Sep-14 9:48am    
kbrandwijk --

So sorry - no I have not actually tried the method you suggested. I should be able to do this today. However, I have been attempting to use the System.Windows.Media.Imaging namespace code here:
http://code.google.com/p/exiv2net/source/browse/trunk/example/MetaInfo.cs
from Andreas Grimme.
Also, this site:
http://www.i-programmer.info/programming/wpf-workings/588-bitmap-codingencoding-and-working-with-metadata.html?start=3
seems to suggest the WPF "inplacemetadatawriter" gets away with re-writing the entire image.

For my application, I take photos with a Canon with a pre-set EXIF structure, then I must add in the GPS tags to the existing image, and then store the tagged image to disk. I want to do this as fast as possible so that I can take images at a fast rate. The picture + download to disk alone takes 1.5 secs. If I have to access the image, copy it, and re-store the image, this may take too long. My thoughts are to compare the timing of both of the methods from the system.media and system.drawing namespaces.
--- Jim
I modified several sets of code that I found around the web.
The code below takes a jpg image file that does not have exif tags for GPS location and adds the GPS information. As a test, the modified code is read back in and test for the presence of the GPS location tags.


C#
using System;
using System.Collections.Generic;
using System.IO;

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace AddGPSTagsToEXIF
{
    class MetadataExample
    {
        // North or South Latitude 
        private const string GPSLatitudeRefQuery = "/app1/ifd/gps/subifd:{ulong=1}"; // ASCII 2
        // Latitude        
        private const string GPSLatitudeQuery = "/app1/ifd/gps/subifd:{ulong=2}"; // RATIONAL 3
        // East or West Longitude 
        private const string GPSLongitudeRefQuery = "/app1/ifd/gps/subifd:{ulong=3}"; // ASCII 2
        // Longitude 
        private const string GPSLongitudeQuery = "/app1/ifd/gps/subifd:{ulong=4}"; // RATIONAL 3
        // Altitude reference 
        private const string GPSAltitudeRefQuery = "/app1/ifd/gps/subifd:{ulong=5}"; // BYTE 1
        // Altitude 
        private const string GPSAltitudeQuery = "/app1/ifd/gps/subifd:{ulong=6}"; // RATIONAL 1

        static void Main( string[] args )
        {
            //original image file
            string originalPath = @"D:\Temp\test.jpg";
            //image file after adding the GPS tags
            string outputPath = @"D:\Temp\testModified.jpg";

            BitmapCreateOptions createOptions = BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile;
            uint paddingAmount = 2048; 

            //open the image file
            using ( Stream originalFile = File.Open ( originalPath, FileMode.Open, FileAccess.Read ) )
            {
                BitmapDecoder original = BitmapDecoder.Create ( originalFile, createOptions, BitmapCacheOption.None );

                //this becomes the new image that contains new metadata
                JpegBitmapEncoder output = new JpegBitmapEncoder ( );

                if ( original.Frames[0] != null && original.Frames[0].Metadata != null )
                {
                    //clone the metadata from the original input image so that it can be modified
                    BitmapMetadata metadata = original.Frames[0].Metadata.Clone ( ) as BitmapMetadata;

                    //pad the metadata so that it can be expanded with new tags
                    metadata.SetQuery ( "/app1/ifd/PaddingSchema:Padding", paddingAmount );
                    metadata.SetQuery ( "/app1/ifd/exif/PaddingSchema:Padding", paddingAmount );
                    metadata.SetQuery ( "/xmp/PaddingSchema:Padding", paddingAmount );

                    //form the new metadata that is to be added
                    double latitude  =   30.0 + 15.0 / 60.0 + 22.0 / 3600.0;
                    double longitude = -(86.0 + 16.0 / 60.0 + 23.0 / 3600.0);
                    double altitude = 44;

                    GPSRational latitudeRational = new GPSRational(latitude);
                    GPSRational longitudeRational = new GPSRational(longitude);
                    metadata.SetQuery(GPSLatitudeQuery, latitudeRational.bytes);
                    metadata.SetQuery(GPSLongitudeQuery, longitudeRational.bytes);
                    if (latitude > 0) metadata.SetQuery(GPSLatitudeRefQuery, "N");
                    else metadata.SetQuery(GPSLatitudeRefQuery, "S");
                    if (longitude > 0) metadata.SetQuery(GPSLongitudeRefQuery, "E");
                    else metadata.SetQuery(GPSLongitudeRefQuery, "W");

                    Rational altitudeRational = new Rational((int)altitude, 1);  //denoninator = 1 for Rational
                    metadata.SetQuery(GPSAltitudeQuery, altitudeRational.bytes);

                  
                    //create the output image using the image data, thumbnail, and metadata from the original image as modified above
                    output.Frames.Add ( 
                        BitmapFrame.Create ( original.Frames[0], original.Frames[0].Thumbnail, metadata, original.Frames[0].ColorContexts ) );
                }

                //save the output image
                using ( Stream outputFile = File.Open ( outputPath, FileMode.Create, FileAccess.ReadWrite ) )
                {
                    output.Save ( outputFile );
                }
            }


            //Check to see if the modified and saved file has the correct added metadata
            using ( Stream savedFile = File.Open ( outputPath, FileMode.Open, FileAccess.Read ) )
            {
                BitmapDecoder output = BitmapDecoder.Create ( savedFile, BitmapCreateOptions.None, BitmapCacheOption.Default );
                //InPlaceBitmapMetadataWriter metadata = output.Frames[0].CreateInPlaceBitmapMetadataWriter ( );
                BitmapMetadata metadata = output.Frames[0].Metadata.Clone() as BitmapMetadata;

                byte[] lat4 = (byte[])metadata.GetQuery(GPSLatitudeQuery);
                byte[] lon4 = (byte[])metadata.GetQuery(GPSLongitudeQuery);
                String latitudeRef  = (String)metadata.GetQuery(GPSLatitudeRefQuery);
                String longitudeRef = (String)metadata.GetQuery(GPSLongitudeRefQuery);
                byte[] altitude = (byte[])metadata.GetQuery(GPSAltitudeQuery);
            }
        }

     }


    //EXIF Rational Type (pack 4-byte numerator and 4-byte denominator into 8 bytes
    public class Rational
    {
        public Int32 _num;     //numerator of exif rational
        public Int32 _denom;   //denominator of exif rational
        public byte[] bytes;   //8 bytes that form the exif rational value

        //form rational from a given 4-byte numerator and denominator
        public Rational(Int32 _Num, Int32 _Denom)
        {
            _num = _Num;
            _denom = _Denom;

            bytes = new byte[8];  //create a byte array with 8 bytes
            BitConverter.GetBytes(_num).CopyTo(bytes, 0);  //copy 4 bytes of num to location 0 in the byte array
            BitConverter.GetBytes(_denom).CopyTo(bytes, 4);  //copy 4 bytes of denom to location 4 in the byte array
        }

        //form rational from an array of 8 bytes
        public Rational(byte[] _bytes)
        {
            byte[] n = new byte[4];
            byte[] d = new byte[4];
            //copy 4 bytes from bytes-array into n-array starting at index 0 from bytes and 0 from n 
            Array.Copy(_bytes, 0, n, 0, 4);
            //copy 4 bytes from bytes-array into d-array starting at index 4 from bytes and 0 from d 
            Array.Copy(_bytes, 4, d, 0, 4);
            //convert the 4 bytes from n into a 4-byte int (becomes the numerator of the rational)
            _num = BitConverter.ToInt32(n, 0);
            //convert the 4 bytes from d into a 4-byte int (becomes the denonimator of the rational)
            _denom = BitConverter.ToInt32(d, 0);

        }

        //convert the exif rational into a double value
        public double ToDouble()
        {
            //round the double value to 5 digits
            return Math.Round(Convert.ToDouble(_num) / Convert.ToDouble(_denom), 5);
        }
    }

    //special rational class to handle the GPS three rational values  (degrees, minutes, seconds)
    public class GPSRational
    {
        public Rational _degrees;
        public Rational _minutes;
        public Rational _seconds;
        public byte[] bytes;  //becomes an array of 24 bytes that represent hrs, minutes, seconds as 3 rationals
        double angleInDegrees;  //latitude or longitude as decimal degrees

        //form the 3-rational exif value from an angle in decimal degrees
        public GPSRational(double angleInDeg)
        {
            //convert angle in decimal degrees to three rationals (deg, min, sec) with denominator of 1
            //NOTE:  this formulation results in a descretization of about 100 ft in the lat/lon position
            double absAngleInDeg = Math.Abs(angleInDeg);
            int degreesInt = (int)(absAngleInDeg);
            absAngleInDeg -= degreesInt;
            int minutesInt = (int)(absAngleInDeg * 60.0);
            absAngleInDeg -= minutesInt / 60.0;
            int secondsInt = (int)(absAngleInDeg * 3600.0 + 0.50);

            //form a rational using "1" as the denominator
            int denominator = 1;
            _degrees = new Rational(degreesInt, denominator);
            _minutes = new Rational(minutesInt, denominator);
            _seconds = new Rational(secondsInt, denominator);

            angleInDegrees = _degrees.ToDouble() + _minutes.ToDouble() / 60.0 + _seconds.ToDouble() / 3600.0;

            // form the 24-byte array representing the 3 rationals
            bytes = new byte[24];
            BitConverter.GetBytes(degreesInt).CopyTo(bytes, 0);
            BitConverter.GetBytes(denominator).CopyTo(bytes, 4);
            BitConverter.GetBytes(minutesInt).CopyTo(bytes, 8);
            BitConverter.GetBytes(denominator).CopyTo(bytes, 12);
            BitConverter.GetBytes(secondsInt).CopyTo(bytes, 16);
            BitConverter.GetBytes(denominator).CopyTo(bytes, 20);
        }

        //Form the GPSRational object from an array of 24 bytes
        public GPSRational(byte[] _bytes)
        {
            byte[] degBytes = new byte[8]; byte[] minBytes = new byte[8]; byte[] secBytes = new byte[8];

            //form the hours, minutes, seconds rational values from the input 24 bytes
            // first 8 are hours, second 8 are the minutes, third 8 are the seconds
            Array.Copy(_bytes, 0, degBytes, 0, 8); Array.Copy(_bytes, 8, minBytes, 0, 8); Array.Copy(_bytes, 16, secBytes, 0, 8);

            _degrees = new Rational(degBytes);
            _minutes = new Rational(minBytes);
            _seconds = new Rational(secBytes);

            angleInDegrees = _degrees.ToDouble() + _minutes.ToDouble() / 60.0 + _seconds.ToDouble() / 3600.0;
            bytes = new byte[24];
            _bytes.CopyTo(bytes, 0);
        }
    }

}
 
Share this answer
 
Comments
Dan Desjardins 27-Sep-16 16:28pm    
Well this definitely does something, but I'm not sure it actually adds valid geotags. I'm trying to figure out the meaning of the following lines:


double latitude = 30.0 + 15.0 / 60.0 + 22.0 / 3600.0;
double longitude = -(86.0 + 16.0 / 60.0 + 23.0 / 3600.0);
double altitude = 44;

is this a set of hard-coded lat lons in decimal, but why the math instead of a simple decimal lat and long (e.g. 43.086282:-89.488111)

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900