18

Within the property window of a JPEG image, there is a tab called 'Summary'. Within this tab, there is a field called 'Comments' I would like to write some c# code which will add a given string to this field e.g "This is a photo".

Does some kind soul out there know how to do this?

Many thanks.

John P
  • 339
  • 1
  • 4
  • 19

6 Answers6

20

Based on other answers I wrote the following class which allows various metadata manipulations. You use it like this:

var jpeg = new JpegMetadataAdapter(pathToJpeg);
jpeg.Metadata.Comment = "Some comments";
jpeg.Metadata.Title = "A title";
jpeg.Save();              // Saves the jpeg in-place
jpeg.SaveAs(someNewPath);  // Saves with a new path

The differences between my solution and the others are not large. Principally I have refactored this to be cleaner. I also use the higher level properties of BitmapMetadata, rather than the SetQuery method.

Here is the full code, which is licensed under the MIT licence. You will need to add references to PresentationCore, WindowsBase, and System.Xaml.

public class JpegMetadataAdapter
{
    private readonly string path;
    private BitmapFrame frame;
    public readonly BitmapMetadata Metadata;

    public JpegMetadataAdapter(string path)
    {
        this.path = path;            
        frame = getBitmapFrame(path);
        Metadata = (BitmapMetadata)frame.Metadata.Clone();
    }

    public void Save()
    {
        SaveAs(path);
    }

    public void SaveAs(string path)
    {
        JpegBitmapEncoder encoder = new JpegBitmapEncoder();
        encoder.Frames.Add(BitmapFrame.Create(frame, frame.Thumbnail, Metadata, frame.ColorContexts));
        using (Stream stream = File.Open(path, FileMode.Create, FileAccess.ReadWrite))
        {
            encoder.Save(stream);
        }
    }

    private BitmapFrame getBitmapFrame(string path)
    {
        BitmapDecoder decoder = null;
        using (Stream stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
        {
            decoder = new JpegBitmapDecoder(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
        }
        return decoder.Frames[0];
    }
}
M. Schena
  • 2,039
  • 1
  • 21
  • 29
Martin Eden
  • 6,143
  • 3
  • 30
  • 33
13

The following code solves my problem and adds comments to a given JPEG image:

public void addImageComment(string imageFlePath, string comments)
    {
        string jpegDirectory = Path.GetDirectoryName(imageFlePath);
        string jpegFileName = Path.GetFileNameWithoutExtension(imageFlePath);

        BitmapDecoder decoder = null;
        BitmapFrame bitmapFrame = null;
        BitmapMetadata metadata = null;
        FileInfo originalImage = new FileInfo(imageFlePath);

        if (File.Exists(imageFlePath))
        {
            // load the jpg file with a JpegBitmapDecoder    
            using (Stream jpegStreamIn = File.Open(imageFlePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
            {
                decoder = new JpegBitmapDecoder(jpegStreamIn, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
            }

            bitmapFrame = decoder.Frames[0];
            metadata = (BitmapMetadata)bitmapFrame.Metadata;

            if (bitmapFrame != null)
            {
                BitmapMetadata metaData = (BitmapMetadata)bitmapFrame.Metadata.Clone();

                if (metaData != null)
                {
                    // modify the metadata   
                    metaData.SetQuery("/app1/ifd/exif:{uint=40092}", comments);

                    // get an encoder to create a new jpg file with the new metadata.      
                    JpegBitmapEncoder encoder = new JpegBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(bitmapFrame, bitmapFrame.Thumbnail, metaData, bitmapFrame.ColorContexts));
                    //string jpegNewFileName = Path.Combine(jpegDirectory, "JpegTemp.jpg");

                    // Delete the original
                    originalImage.Delete();

                    // Save the new image 
                    using (Stream jpegStreamOut = File.Open(imageFlePath, FileMode.CreateNew, FileAccess.ReadWrite))
                    {
                        encoder.Save(jpegStreamOut);
                    }
                }
            }
        }
    }

This is essentially a lightly modified version of the code found under the link which Konamiman kindly supplied.

Please be aware that to make this work you will need to add .NET references to PresentationCore and WindowsBase. If using Visual Studio 2008, this can be achieved via the following:

  1. Right click on your project in the Solution Explorer

  2. From the drop down list, select Add 'Reference...'

  3. From the new box which opens, select the '.NET' tab

  4. Scroll to the two references mentioned above and on each, click ok

Many thanks to both danbystrom and Konamiman for your help in this matter. I really appreciate the quick response.

John P
  • 339
  • 1
  • 4
  • 19
  • You should vote up and/or accept their answers if you found them helpful. Cheers! – RickL Nov 19 '09 at 17:13
  • Oops, I made a small mistake with my solution above. I should have said that you need to add the PresentationCore reference, not the System.Core Ps. Thanks for the heads-up Rick! I will do that straight away. – John P Nov 20 '09 at 12:14
  • 4
    Given that jpeg is a lossy format, would you expect to lose quality by adding metadata using this method, or does this just modify the metadata without affecting the picture? – Derreck Dean Mar 30 '12 at 18:15
2

The easy part:

Add this property item:

var data = System.Text.Encoding.UTF8.GetBytes( "Some comments" );
PropertyItem pi;
*** create an empty PropertyItem here
pi.Type = 2;
pi.Id = 37510;
pi.Len = data.Length;
pi.Value = data;

To the Image's PropertItems collection.

The somewhat more cumbersome part: How do you create a new PropertyItem, since it has no public constructor?

The common "trick" is to have an empty image lying around from which you can steal a PropertyItem. sigh

Dan Byström
  • 9,067
  • 5
  • 38
  • 68
  • 1
    and how do you associate the stolen PropertyItem to the real image? Is it a read-write property/variable? – Vinko Vrsalovic Nov 18 '09 at 11:00
  • realImage.PropertyItems.Add( emptyImage.PropertyItems[0] ); – Dan Byström Nov 18 '09 at 11:21
  • 2
    Frustrating. From msdn: "A PropertyItem is not intended to be used as a stand-alone object. A PropertyItem object is intended to be used by classes that are derived from Image. A PropertyItem object is used to retrieve and to change the metadata of existing image files, not to create the metadata. Therefore, the PropertyItem class does not have a defined Public constructor, and you cannot create an instance of a PropertyItem object." – JYelton Mar 30 '10 at 17:45
  • 1
    This answer provides a solution for how to create a new instance of a PropertyItem: https://stackoverflow.com/a/25162782/606602 – Ciaran Gallagher Aug 09 '19 at 11:04
1

Thanks to the answers here, I've coded a solution to set a comment using memory only:

public static Image SetImageComment(Image image, string comment) {
  using (var memStream = new MemoryStream()) {
    image.Save(memStream, ImageFormat.Jpeg);
    memStream.Position = 0;
    var decoder = new JpegBitmapDecoder(memStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
    BitmapMetadata metadata;
    if (decoder.Metadata == null) {
      metadata = new BitmapMetadata("jpg");
    } else {
      metadata = decoder.Metadata;
    }

    metadata.Comment = comment;

    var bitmapFrame = decoder.Frames[0];
    BitmapEncoder encoder = new JpegBitmapEncoder();
    encoder.Frames.Add(BitmapFrame.Create(bitmapFrame, bitmapFrame.Thumbnail, metadata, bitmapFrame.ColorContexts));

    var imageStream = new MemoryStream();
    encoder.Save(imageStream);
    imageStream.Position = 0;
    image.Dispose();
    image = null;
    return Image.FromStream(imageStream);
  }
}

Don't forget to dispose the image that is returned by this method. (For example after saving the image to a file)

Bas de Raad
  • 556
  • 1
  • 6
  • 15
0

Thanks to the previous tips I was able to put the following together. I've tested it and it seems to work. One of the biggest stumbling blocks was determining the Id needed for the field you want to assign.

string fileName = "c:/SomeImage.jpg";
// Retrieve the Image
System.Drawing.Image originalImage = System.Drawing.Image.FromFile(fileName);

// Get the list of existing PropertyItems. i.e. the metadata
PropertyItem[] properties = originalImage.PropertyItems;

// Create a bitmap image to assign attributes and do whatever else..
Bitmap bmpImage = new Bitmap((Bitmap)originalImage);

// Don't need this anymore
originalImage.Dispose();

// Get / setup a PropertyItem
PropertyItem item = properties[0]; // We have to copy an existing one since no constructor exists

// This will assign "Joe Doe" to the "Authors" metadata field
string sTmp = "Joe DoeX"; // The X will be replaced with a null.  String must be null terminated.
var itemData = System.Text.Encoding.UTF8.GetBytes(sTmp);
itemData[itemData.Length - 1] = 0;// Strings must be null terminated or they will run together
item.Type = 2; //String (ASCII)
item.Id = 315; // Author(s), 315 is mapped to the "Authors" field
item.Len = itemData.Length; // Number of items in the byte array
item.Value = itemData; // The byte array
bmpImage.SetPropertyItem(item); // Assign / add to the bitmap

// This will assign "MyApplication" to the "Program Name" field
sTmp = "MyApplicationX";
itemData = System.Text.Encoding.UTF8.GetBytes(sTmp);
itemData[itemData.Length - 1] = 0; // Strings must be null terminated or they will run together
item.Type = 2; //String (ASCII)
item.Id = 305; // Program Name, 305 is mapped to the "Program Name" field
item.Len = itemData.Length;
item.Value = itemData;
bmpImage.SetPropertyItem(item);

// Save the image
bmpImage.Save(fileName, System.Drawing.Imaging.ImageFormat.Jpeg);

//Clean up
bmpImage.Dispose();
0

A variant on Peter Kistler's solution to set Title, Subject and Comment. I found I had to create items as Unicode byte array (type 1) and the IDs for Title, Subject and Comment are the same as for EXIF XPTitle, XPSubject and XP Comment. sFileOut can be the same as sFile.

public static void SetWindowsTags2(string sFile, string sFileOut, string Title = "", string Subject = "", string Comment = "", bool bShowError = false)
    {
        try
        {

            // Retrieve the Image
            System.Drawing.Image originalImage = System.Drawing.Image.FromFile(sFile);

            // Get the list of existing PropertyItems. i.e. the metadata
            PropertyItem[] properties = originalImage.PropertyItems;

            /*foreach (PropertyItem propItem in properties)
            {
                string sTag = System.Text.Encoding.UTF8.GetString(propItem.Value);
                string sItem = sTag.Replace("\0", string.Empty);

                Debug.Print(propItem.Id.ToString() + ", " +  propItem.Type + ", " + sItem);
            }*/

            // Create a bitmap image to assign attributes and do whatever else..
            Bitmap bmpImage = new Bitmap((Bitmap)originalImage);

            // Don't need this anymore
            originalImage.Dispose();

            // Get / setup a PropertyItem
            PropertyItem item = properties[0]; // We have to copy an existing one since no constructor exists

            var itemData = System.Text.Encoding.Unicode.GetBytes(Title);
            itemData[itemData.Length - 1] = 0;// Strings must be null terminated or they will run together
            item.Type = 1; //Unicode Byte Array
            item.Id = 40091; // Title ID
            item.Len = itemData.Length; // Number of items in the byte array
            item.Value = itemData; // The byte array
            bmpImage.SetPropertyItem(item); // Assign / add to the bitmap


            itemData = System.Text.Encoding.Unicode.GetBytes(Subject);
            itemData[itemData.Length - 1] = 0; // Strings must be null terminated or they will run together
            item.Type = 1; //Unicode Byte Array
            item.Id = 40095; // subject
            item.Len = itemData.Length;
            item.Value = itemData;
            bmpImage.SetPropertyItem(item); // Assign / add to the bitmap


            itemData = System.Text.Encoding.Unicode.GetBytes(Comment);
            itemData[itemData.Length - 1] = 0; // Strings must be null terminated or they will run together
            item.Type = 1; ////Unicode Byte Array
            item.Id = 40092; // comment
            item.Len = itemData.Length;
            item.Value = itemData;
            bmpImage.SetPropertyItem(item); // Assign / add to the bitmap


            // Save the image
            bmpImage.Save(sFileOut, System.Drawing.Imaging.ImageFormat.Jpeg);

            //Clean up
            bmpImage.Dispose();

        }
        catch (Exception Ex)
        {

        }

    }
SimonKravis
  • 553
  • 1
  • 3
  • 24