locked
Reading and Writing image "Rating" metadata RRS feed

  • Question

  • I'm looking for a way to read and modify "Rating" metadata that Vista uses to rate image files.

    I'm able to read the rating although I noticed that the rating is written in several places in metadata... and I'm not sure which Vista is using... it looks like all of them are changed at the same time and are kept in sync... so do I have to modify all of them to keep them in sync?

    /xmp/Rating

    /xmp/xap:Rating

    /app1/{uint=0}/{uint=18246}

    When I'm trying to modify the rating... nothing happens... I'm using this code

    Stream fileStream = new System.IO.FileStream(path.LocalPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);

    BitmapDecoder decoder = BitmapDecoder.Create(this.fileStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);

    InPlaceBitmapMetadataWriter writer = decoder.Frames[0].CreateInPlaceBitmapMetadataWriter();

    writer.SetQuery("/xmp/xap:Rating", "4");

    I get no exceptions... so what's wrong?

    Sunday, June 11, 2006 5:00 PM

Answers

  • What's likely happening is that the file doesn't have enough space in it to do an in-place edit. Typical pattern of usage with the InPlaceBitmapMetadataWriter is as follows:

    InPlaceBitmapMetadataWriter writer = decoder.Frames[0].CreateInPlaceBitmapMetadataWriter();

    writer.SetQuery("/xmp/xap:Rating", "4");

    if(!writer.TrySave())

    {

        // In place metadata editing failed.

        // You will need to write out the metadata using the BitmapMetadata class and fully re-encoding the image (ie: grab the Frames from the decoder)

    }

    One thing to note is that if the TrySave fails, some of the data may have been written out to the stream already. So, Ideally you should try to do the TrySave on a temporary in memory stream rather than the original file.

    Friday, July 28, 2006 8:34 PM

All replies

  • What's likely happening is that the file doesn't have enough space in it to do an in-place edit. Typical pattern of usage with the InPlaceBitmapMetadataWriter is as follows:

    InPlaceBitmapMetadataWriter writer = decoder.Frames[0].CreateInPlaceBitmapMetadataWriter();

    writer.SetQuery("/xmp/xap:Rating", "4");

    if(!writer.TrySave())

    {

        // In place metadata editing failed.

        // You will need to write out the metadata using the BitmapMetadata class and fully re-encoding the image (ie: grab the Frames from the decoder)

    }

    One thing to note is that if the TrySave fails, some of the data may have been written out to the stream already. So, Ideally you should try to do the TrySave on a temporary in memory stream rather than the original file.

    Friday, July 28, 2006 8:34 PM
  • // You will need to write out the metadata using the BitmapMetadata class and fully re-encoding the image (ie:
    grab the Frames from the decoder)

    ^ This is the bit i am stuck on. I can't work out how to get a copy of the metadata that isn't frozen.

    I clone the BitmapFrame which is not fFrozen, but then its Metadata property is frozen.

    I tried setting the encoder.Metadata property explicitly, but that appears to be the wrong location as the jpeg doesn't support global meta data.

    Any chance we can get a sample that demonstrates how to do this?

    The BitmapMetadata sample in the Windows SDK demonstrates only the most simple usage scenario.

    Tuesday, December 26, 2006 10:05 PM
  • yes, right!, an example would be great.

    thomas
    Thursday, January 18, 2007 12:13 PM
  • Hello, here is a sample test

    Stream JpegStreamIn = new FileStream("iptcIN.jpg", FileMode.Open, FileAccess.Read, FileShare.Read);
    JpegBitmapDecoder JpegDecoder = new JpegBitmapDecoder(JpegStreamIn,
            BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
    FileStream JpegStreamOut = new FileStream("IPTCOUT.jpg", FileMode.Create);
    JpegBitmapEncoder JpegEncoder = new JpegBitmapEncoder();

    BitmapFrame JpegFrame = BitmapFrame.Create(JpegDecoder.Frames[0]);
    JpegEncoder.Frames.Add(JpegFrame);
    BitmapMetadata JpegMeta = (BitmapMetadata)JpegFrame.Metadata;

    JpegMeta.Copyright = "COPYRIGHT";

    string[] MC = new string[JpegMeta.Keywords.Count + 1];
    JpegMeta.Keywords.CopyTo(MC, 0);
    MC[MC.Length - 1] = "NEW-KEYWORD";
    JpegMeta.Keywords = new ReadOnlyCollection<string>(MC);

    JpegEncoder.Save(JpegStreamOut);

    Where can I find a complète list of metadatas string identifiers (like "/xmp/Rating" "/xmp/xap:Rating" and "/app1/{uint=0}/{uint=18246}") ?? The only example I found in doc is "/text/Description" !

    Thanks.

    Jean-Luc

    Friday, January 19, 2007 9:54 PM
  • look here
    thomas

    Saturday, January 20, 2007 8:43 AM
  • i have another question. when i set:

                //System.Author
                string[] _author = new string[1];
                _author[0] = "John Doe";
                JpegMeta.Author = new ReadOnlyCollection<string>(_author);

    then this works fine. when i set the single values (in order ---> here) like this:

    Order Path Disk Format Required
    1 /xmp/<xmpseq>dc:creator A vector of Unicode strings. The XMP format of the string is xmpseq. Yes
    2 /xmp/tiff:artist A vector of Unicode strings. Yes
    3 /app13/irb/8bimiptc/iptc/by-line An ASCII vector No
    4 /app1/ifd/{ushort=315} An ASCII vector. Delimited. No
    5 /app1/ifd/{ushort=40093} Unicode bytes. Semicolon delimited. No

    "/xmp/tiff:artist" works fine with a string like "John Doe" ...
    but how to set "xmp/<xmpseq>dc:creator" ?!? (with a hashtable or what ...?)

    thomas
    Saturday, January 20, 2007 2:49 PM
  • thanks for your sample Jean-Luc.
    This is my code that doesn't work. The bmpWriter .TrySave() always fails.
    I am using a memoryStream as suggested by Robert in his earlier post.
    If I ignore the TrySave failure and save anyway, then the jpeg saves, but the meta data doesn't reflect the new value.

    I also tried using the BitmapFrame.Create ctor as you do to seed the encoder, however in that case the
    CreateInPlaceBitmapMetadataWriter method returns null :|

    If I use your technique of editing the
    BitmapMetadata directly  then the meta data is not updated with the new value that I set.

    Stream fileStream = File.Open(photo.FilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
                BinaryReader br = new BinaryReader(fileStream);

                MemoryStream memoryStream = new MemoryStream(br.ReadBytes((int) fileStream.Length));

                string outputFilename = photo.FilePath + ".bak.jpg";

                FileStream outputFile = File.Open(outputFilename, FileMode.Create);

                using (memoryStream)
                {
                    BitmapDecoder decoder = BitmapDecoder.Create(memoryStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
                    BitmapFrame sourceFrame = decoder.Frames[0];

                    JpegBitmapEncoder encoder = new JpegBitmapEncoder();
                    encoder.Frames.Add(sourceFrame);

                    InPlaceBitmapMetadataWriter bmpWriter = sourceFrame.CreateInPlaceBitmapMetadataWriter();

                    bmpWriter.Title = photo.Title;
                    
                    if (!bmpWriter.TrySave())
                    {
                        throw new Exception("It didn't work");
                    }

                    encoder.Save(outputFile);
                }

                outputFile.Close();
    Tuesday, January 30, 2007 7:43 AM
  • Thank you thspoe.

    code jockey: I think you can't use "BitmapCacheOption.None" if you want to use InPlaceBitmapMetadaWriter.
    Here's remark  in MSDN:

    If BitmapMetadata is not associated with a BitmapFrame that is obtained through a BitmapDecoder, the attempt to write metadata by using the in-place writer will fail. Successful write operations apply the metadata directly to the image stream by means of a decoder.

    In-place metadata updates fail if an attempt is made to write metadata that a given bitmap format does not support.

    To perform in-place metadata edits, the bitmap must be decoded using either the Default or OnDemand cache options. OnLoad does not guarantee access to the metadata stream needed for in-place metadata edits.

     Also, if you want to create a copy of your photo with modified metadatas, it's not usefull to use InPlaceBitmapMetadataWriter, you can do that like in my sample above.
    I think it's usefull only if you want to save metadatas directely in original file.

    But... I don't like my sample because the picture is decoded and encoded in another file. The encoder has a QualityLevel property wich is 75 (%) by default. If you compare in and out files, there are differences (with a zoom in Photoshop). If you use 100 for QualityLevel, out file size is +/- 3x biger than original. If you use repetitive update you loose quality, it's not acceptable.

    The solution is in this case InPlaceMetadataWriter but I've never seen a sample wich is realy working!
    TrySave() return true before any modification and return false after a modification. If you use sample from MS:

    if (pngInplace.TrySave() == true)
    {
     pngInplace.SetQuery("/Text/Description", "Have a nice day.");
    }
    pngStream.Close();

    It seems TrySave is used to test if it's possible to save, not to append modifications to file.
    Or I dont' understand? (It's possible, i'm so stupid....)

    If anybody has some working samples with InPlaceMetadataWriter, please share it !

    Jean-Luc

    PS: sorry for my poor english, I'm from Belgium and I speak french.

     

     

    Tuesday, January 30, 2007 10:50 AM
  • Yes looks like you are right according to the doco Jean Luc - however I just tried it with OnDemand and that didn't work either.

    WRT the TrySave() method - I have been working on the assumption that the MSDN doco is wrong.
    For several reasons
    1) As you say it does nothing when you call it before the SetQuery
    2) Robert's sample above shows usage contrary to MSDN doco
    3) Using reflector shows this

    [SecurityCritical]
    public bool TrySave()
    {
    int num1;
    Invariant.Assert(this._fmeHandle != null);
    lock (base.SyncObject)
    {
    num1 = UnsafeNativeMethods.WICFastMetadataEncoder.Commit(this._fmeHandle);
    }
    return HRESULT.Succeeded(num1);
    }
    The commit statement looks awfully like a 'write' operation.

    I don't think any of us here are stupid, more that the doco is a bit lacking.
    Looking at more of the SDK samples today, it does look a bit rushed.
    There are lots of readme.htm's for samples that are just blank templates that the MS developers didn't bother filling out!

    ps: Your English is far better than my French. au revior!


    Tuesday, January 30, 2007 12:17 PM
  • Here is a sample code to retrieve all metadata from an image file. In a form add a button and a RichTextBox. It's not my code, I don't remember wher I found it, sorry.

            private void button3_Click(object sender, EventArgs e)
            {
                richTextBox1.Clear();
                using (Stream fileStream = File.Open("iptcx.jpg", FileMode.Open))
                {
                    BitmapDecoder decoder = BitmapDecoder.Create(fileStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
                    PrintMetadata(decoder.Frames[0].Metadata, string.Empty);
                }
            }

            private void PrintMetadata(System.Windows.Media.ImageMetadata metadata, string fullQuery)
            {
                BitmapMetadata theMetadata = metadata as BitmapMetadata;
                if (theMetadata != null)
                {
                    foreach (string query in theMetadata)
                    {
                        string tempQuery = fullQuery + query;
                        // query string here is relative to the previous metadata reader.
                        object o = theMetadata.GetQuery(query);
                        richTextBox1.Text += "\n" + tempQuery + ", " + query + ", " + o;
                        BitmapMetadata moreMetadata = o as BitmapMetadata;
                        if (moreMetadata != null)
                        {
                            PrintMetadata(moreMetadata, tempQuery);
                        }
                    }
                }
            }

    thspoe : if you change Authors property, /xmp/dc:creator is changed.
    If you want to change these values individualy, you can do that like:

                JpegMeta.SetQuery("/xmp/dc:creator/{ulong=0}","Auth 1");
                JpegMeta.SetQuery("/xmp/dc:creator/{ulong=1}","Auth 2");
                JpegMeta.SetQuery("/xmp/dc:creator/{ulong=2}","Auth 3");
                JpegMeta.SetQuery("/xmp/dc:creator/{ulong=3}","Auth 4");

    Jean-Luc

    Tuesday, January 30, 2007 12:32 PM
  • Hello Jean-Luc,

    thank you!

    Thomas

     

    Tuesday, January 30, 2007 1:34 PM
  • OK, so yesterday I worked something out that I didn't fully realise.
    The TrySave / InPlaceMetadataWriter will only work if there is enough space in the file to save metadata. That is why the method is called TrySave() - it won't always work (see here http://msdn2.microsoft.com/en-us/library/aa968941.aspx)
    . It is there to be a faster alternative to doing a full resave of the file - which you have to be prepared to do in order to get enough space for the metadata.

    Robert W was actually telling us this in his reply - I just didn't understand it fully until yesterday.

    Following from that, it means no matter what, we still have to get the full encoding to work to perform as a last resort on any file that TrySave fails on. The loss of jpeg quality is a concern - but before I get to worrying about that, I still haven't got any code to actually reliably work doing the save :|
    My current hurdle is I get HRESULT exceptions when the encoder tries to save.

    EDIT: Interestingly the HRESULT error is different in XP and Vista. In XP it is something like 0xC000005. In Vista it is 0x88982F71 and comes with additional message of "Cannot write to the stream." (but if I don't try and edit any meta data it writes to the stream fine)

    Below are three different methods. The first two are based on some rough samples that Robert sent me off line. The third is your sample JL.
    The code fully compiles as a console app - you just need to add references - see the usage doco below.

    If it helps anyone get to the next step and can get the files to encode without these damn HRESULT errors (or the codec does not support this or that) then let us know!



    using System;
    using System.IO;
    using System.Collections.ObjectModel;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows.Media.Imaging;
    using System.Globalization;
    using System.Diagnostics;

    /*
     * Usage
     * 1) Create a new Console Application solution
     * 2) Paste this into the Program.cs file
     * 3) Add References to PresentationCore and WindowsBase
     * 4) Compile and drop a "test.jpg" file into the Debug output folder
     */
    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                if (args.Length == 0)
                {
                    args = new string[1] { @"IMG_8968.JPG" };
                }

                if (args.Length > 0)
                {
                    MetaWriter metaWriter = new MetaWriter(args[0]);
                    //metaWriter.ModifyExistingData();
                    //metaWriter.WriteCleanMetaData();
                    metaWriter.JLWrite();
                }
                else
                {
                    Console.WriteLine("Specify a file as an argument");
                }
            }
        }

        /// <summary>
        /// Note http://msdn2.microsoft.com/en-us/library/aa968941.aspx
        /// indicates that the whole TrySave() with InPlaceWriter isn't guaranteed to always work.
        /// It is a fast way of doing the encoding.
        /// So we have to fall back to reencoding the whole thing again as a last resort if try save fails...
        /// ... so lets get that working first
        /// </summary>
        class MetaWriter
        {
            string _fileIn;
            string _fileOut;

            public MetaWriter(string filename)
            {
                _fileIn = filename;
                _fileOut = filename + ".out.jpg";
            }

            public void ModifyExistingData()
            {
                // Setup input/decoder
                Stream inputStream = new System.IO.FileStream(_fileIn, FileMode.Open, FileAccess.Read, FileShare.Read);
                JpegBitmapDecoder decoder = new JpegBitmapDecoder(inputStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
                BitmapFrame inputFrame = decoder.Frames[0];
                BitmapMetadata inputMetadata = inputFrame.Metadata as BitmapMetadata;

                Notify("Input Title = {0}", inputMetadata.Title);
                Notify("Input DateTime = {0}", inputMetadata.DateTaken);

                if (inputMetadata.Keywords != null)
                {
                    for (int i = 0; i < inputMetadata.Keywords.Count; i++)
                    {
                        Notify("Input Keyword = {0}", inputMetadata.KeywordsIdea);
                    }
                }

                // Setup output/encoder
                Stream outputStream = new System.IO.FileStream(_fileOut, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
                outputStream.SetLength(0);
                JpegBitmapEncoder encoder = new JpegBitmapEncoder();

                // Set the meta data
                string[] authorArray = { "abc"};
                string[] keywordArray = { "yesterday", "today", "Debug time " + DateTime.Now.ToString("HH:mm:ss") };
                String dateTimeString = DateTime.Now.ToString(DateTimeFormatInfo.InvariantInfo);

                ReadOnlyCollection<String> collectionAuthors = (new ReadOnlyCollection<String>(authorArray));
                ReadOnlyCollection<String> collectionKeywords = (new ReadOnlyCollection<String>(keywordArray));

                // This following works until we try to edit any of the data.
                // At which point, we get "HRESULT: 0xC0000005" failure on the encoder.Save() method
                // (We can't try and modify inputMetadata directly since it is frozen)
                BitmapMetadata outputMetadata = inputMetadata.Clone();

                // Mucking around trying to get stuff to work by adding padding
                //outputMetadata.SetQuery("/app1/ifd/PaddingSchema:padding", (UInt32)4096);
                //outputMetadata.SetQuery("/app13/ifd/PaddingSchema:padding", (UInt32)4096);
                //outputMetadata.SetQuery("/xmp/PaddingSchema:padding", (UInt32)4096);

                // If we uncomment these lines we get the "HRESULT: 0xC0000005" failure  on save
                //outputMetadata.Author = collectionAuthors;
                outputMetadata.Keywords = collectionKeywords;
                //outputMetadata.DateTaken = dateTimeString;

                //outputMetadata.SetQuery("System.Title", "monkey");

                /*
                 * Uncommenting these lines give "This codec does not support the specified property."
                 * The paths came from http://msdn.microsoft.com/library/default.asp?url=/library/en-us/DMD_Imag/htm/photo_metadata_policy__mvoo.asp
                 */
                outputMetadata.SetQuery("/xmp/<xmpbag>dc:subject", collectionKeywords);
                //outputMetadata.SetQuery("/app13/arb/8bimiptc/iptc/keywords", collectionKeywords);
                //outputMetadata.SetQuery("/app1/ifd/{ushort=18247}", collectionKeywords);
                //outputMetadata.SetQuery("/app1/ifd/{ushort=40094}", collectionKeywords);

                // Create output
                BitmapFrame outputFrame = BitmapFrame.Create(inputFrame, null, outputMetadata, null);
                encoder.Frames.Add(outputFrame);

                encoder.Save(outputStream);

                outputStream.Flush();

                outputStream.Close();
            }

            public void WriteCleanMetaData()
            {
                Stream inputStream = new System.IO.FileStream(_fileIn, FileMode.Open, FileAccess.Read, FileShare.Read);
                JpegBitmapDecoder decoder = new JpegBitmapDecoder(inputStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);

                Stream outputStream = new System.IO.FileStream(_fileOut, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
                outputStream.SetLength(0);

                JpegBitmapEncoder encoder = new JpegBitmapEncoder();
                BitmapMetadata metadata = new BitmapMetadata("jpg");

                // this works fine - setting DateTime in EXIF
                metadata.SetQuery("/app1/ifd/exif/{ushort=36867}", DateTime.Now.ToString(DateTimeFormatInfo.InvariantInfo));

                // this works fine - creating a by line in IPTC
                metadata.SetQuery("/app13/irb/8bimiptc/iptc/by-line", "Monkey");

                // This gives HRESULT: 0xC0000005 when saving (It should write to XMP??)
                //metadata.SetQuery("System.Author", "Monkey");

                // Not sure what these are writing
                metadata.SetQuery("/app1/ifd/{uint=1000}", 12345);
                metadata.SetQuery("/app1/ifd/{uint=1001}", 23456);
                metadata.SetQuery("/app1/ifd/{uint=1002}", 34567);

                // Make space in the metadata header for our data
                metadata.SetQuery("/app1/ifd/PaddingSchema:padding", (UInt32)4096);

                encoder.Frames.Add(BitmapFrame.Create(decoder.Frames[0], null, metadata, null));

                encoder.Save(outputStream);

                outputStream.Flush();

                outputStream.Close();
            }

            /// <summary>
            /// Jean-Luc sample
            /// </summary>
            public void JLWrite()
            {
                Stream JpegStreamIn = new FileStream(_fileIn, FileMode.Open, FileAccess.Read, FileShare.Read);
                JpegBitmapDecoder JpegDecoder = new JpegBitmapDecoder(JpegStreamIn,
                        BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
                FileStream JpegStreamOut = new FileStream(_fileOut, FileMode.Create);
                JpegBitmapEncoder JpegEncoder = new JpegBitmapEncoder();

                BitmapFrame JpegFrame = BitmapFrame.Create(JpegDecoder.Frames[0]);
                JpegEncoder.Frames.Add(JpegFrame);
                BitmapMetadata JpegMeta = (BitmapMetadata)JpegFrame.Metadata;

                JpegMeta.Copyright = "COPYRIGHT";

                string[] MC = new string[JpegMeta.Keywords.Count + 1];
                JpegMeta.Keywords.CopyTo(MC, 0);
                MC[MC.Length - 1] = "NEW-KEYWORD";
                JpegMeta.Keywords = new ReadOnlyCollection<string>(MC);

                JpegEncoder.Save(JpegStreamOut);
            }

            private void Notify(string format, params object[] args)
            {
                Notify(String.Format(format, args));
            }

            private void Notify(string message)
            {
                Trace.WriteLine(message);
                Console.WriteLine(message);
            }
        }
    }

    Thursday, February 1, 2007 7:41 AM
  • Thank you Code Jockey for your code.

    Strange but your code don't run correctely if it run in a console app. I get exception "Invalid cast" when I access metadata's properties. No problem with the same code in a Windows form app. (Yesyes, my config is strange too :o)

    I understand why in-place editing don't work. So I don't have enough space in photo files.

    Here is my investigation results:
    Some properties (like Authors and Keywords) can be modified with 3 différent ways.  Keep in mind you get a photo without metadata inside, so you must create them.

    1) with BitmapMetadata object's properties

    if (!JpegMeta.ContainsQuery("/xmp/Authors"))
    {
        string[] authors = new string[2] { "Auth1", "Auth2" };
        JpegMeta.Author = new ReadOnlyCollection<string>(authors);
    }

    2) with SetQuery

    if (!JpegMeta.ContainsQuery("/xmp/Authors"))
    {
        string[] authors = new string[2] { "Auth1", "Auth2" };
        JpegMeta.SetQuery("System.Author", authors);
    }

    3) second way with SetQuery

    if (!JpegMeta.ContainsQuery("/xmp/Authors"))
    {
        string[] authors = new string[2] { "Auth1", "Auth2" };
        JpegMeta.SetQuery("/xmp/dc:creator", new BitmapMetadata("xmpseq"));    // use "xmpbag" for keywords
        JpegMeta.SetQuery("/xmp/dc:creator/{ulong=0}", authors[0]);
        JpegMeta.SetQuery("/xmp/dc:creator/{ulong=1}", authors[1]);

    }

    The third way is the only one if you want to set "/xmp/photoshop:SupplementalCategories". 

    string[] cat = new string[2] { "C1", "C2" };
    JpegMeta.SetQuery("/xmp/photoshop:SupplementalCategories", new BitmapMetadata("xmpbag"));
    JpegMeta.SetQuery("/xmp/photoshop:SupplementalCategories/{ulong=0}", cat[0]);
    JpegMeta.SetQuery("/xmp/photoshop:SupplementalCategories/{ulong=1}", cat[1]);

    JpegMeta.SetQuery("/app13/{ushort=0}/{ulonglong=61857348781060}/{ushort=1}/{str=Supplemental Category}", cat);

    Well, I think I have everything to begin, even I can't use in-place editing.

    Jean-Luc

    Thursday, February 1, 2007 3:26 PM
  •  See next post :)

    Friday, February 2, 2007 1:04 AM
  • It seems that there is a much simpler way to save metadata to jpeg image, by assigning to the properties of the bitmapmetadata object.

    Here is a code sample for writing the new metadata:

    Dim strm As FileStream = New FileStream("..\Picture 003.jpg", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)

    Dim decoder As JpegBitmapDecoder = New JpegBitmapDecoder(strm, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default)

    Dim m as BitmapMetadata = CType(decoder.Frames(0).Metadata, BitmapMetadata)

    Dim output_jpgStream as FileStream= New FileStream("output.jpg", FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite)

    Dim jpgEncoder As JpegBitmapEncoder = New JpegBitmapEncoder()

    Dim jpgMetadata as BitmapMetadata= New BitmapMetadata("jpg")

    jpgMetadata.CameraManufacturer = m.CameraManufacturer

    Dim l() As String = {"foo", "bar"}

    Dim col As System.Collections.ObjectModel.ReadOnlyCollection(of String) = New System.Collections.ObjectModel.ReadOnlyCollection(Of String)(New List(Of String)(l))

    jpgMetadata.Keywords = col

    jpgMetadata.Comment = "this is my comment!!!!!"

    jpgEncoder.Frames.Add(BitmapFrame.Create(decoder.Frames(0), Nothing, jpgMetadata, Nothing))

    jpgEncoder.Save(output_jpgStream)

    output_jpgStream.Flush()

    output_jpgStream.Close()

    strm.Close()

     

    Here is the code to read the metadata from the saved file:

     

    Dim strm As FileStream = New FileStream("..\Picture 0031.jpg", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)

    Dim decoder As JpegBitmapDecoder = New JpegBitmapDecoder(strm, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default)

    Dim frame = decoder.Frames(0)

    Dim metadata As BitmapMetadata = CType(frame.Metadata, BitmapMetadata)

    Dim str As String = ""

    For Each s In metadata.Keywords

        str = str & " " & s

    Next

    MsgBox("The Description metadata of this image is: " + str)

     

    Hope it helps

    Avner

     

    Friday, February 2, 2007 2:15 AM
  • Hello Avner.

    Yes, assigning to the properties of the BitmapMetadata object is one of the 3 possibilities and it's the simplest way, but if you want manipulate category and supplementalcategories (and many other), you have not corresponding properties in BitmapMetada object. See my previous post for sample.

    Sharing code and sample allways help. Thank you

    Jean-Luc

     

    Friday, February 2, 2007 8:27 AM
  • Avner - Thanks for your sample - but JL is correct - the properties exposed by the metadata object don't encompass all the metadata fields available that one may want to get and set. I for example want to be able to get and set the GPS coordinates in the EXIF headers. So the SetQuery/GetQuery methods are provided to make the library more flexible.

    The other problem is that we should preserve all existing meta data in the image that we don't know about - so ideally we should use a clone of the source bitmapmeta data (as per the ModifyExistingData() method in my prior sample).
    Otherwise we will be throwing away all the EXIF data that the camera inserted like focal length, etc if we use a simple BitmapMetadata constructor and don't explicitly copy all the values over.

    JL - How do you know what literals to put in the constructor for BitmapMetaData? The MSDN documentation (http://msdn2.microsoft.com/en-us/library/system.windows.media.imaging.bitmapmetadata.bitmapmetadata.aspx) only explicitly lists "gif", "jpg", "png", or "tiff" - but nothing else - but then the sample (in the MSDN page) then goes and uses "exif"!

    (I'm not saying you are wrong, I think you are right, just wondering how you know what are the correct values are?).

    At the end of the day although this is incredibly frustrating I think what we are trying to achieve is possible since the .NET code all wraps the new WIC library, and I suspect that is what Vista photo gallery uses - and I know it is what PhotoInfo uses....sigh.

    PS: JL - I just saw that recompression issue you mentioned before when I ran some test code. thats really bad. I suppose there must be a work around though.

    PPS: Sorry for the font craziness. The rich text editor doesn't seem to work so well after some cut and pastes.

    Friday, February 2, 2007 11:46 AM
  • Hello Code Jockey

    For litterals to put in BitmapMetadata constructor, I get them from Photo Metadata Policy page. For some system properties, in path column there are lines like "/xmp/<xmpbag>dc:subject" or "/xmp/<xmpseq>dc:creator"

    Jean-Luc

    Friday, February 2, 2007 12:07 PM
  • did anyone get anywhere with this?
    I pretty much gave up - hoping for an MSDN article or something.
    Monday, February 26, 2007 10:59 AM
  • Can someone from MSFT confirm whether reading/writing metadata is actually possible in the framework?
    Obviously its possible in WIC since Vista's Photo Gallery achieves it but I think someone has dropped the ball in interfacing it to .NET cause there is not a single working sample available.

    Here is someone else who hasn't found a solution
    http://bloggingabout.net/blogs/erwyn/archive/2007/03/18/diving-into-c-for-flickr-metadata-synchr-v0-6-0-0.aspx


    Saturday, May 26, 2007 12:25 AM
  • Reading certainly seems possible.

    But writing is only possible if there's enought space in the Metadata area of the file and this is where most people, including myself are becoming unstuck.

    Friday, June 1, 2007 4:27 PM
  • yes you are correct. I was ranting in my frustration. Reading the XMP works ok - its the writing that is busted.
    Monday, June 4, 2007 9:59 AM
  • Hi I have just found this article, it doesn't contain a solution but kind of a good conclusion

    http://www.vsj.co.uk/articles/display.asp?id=649
    Monday, June 18, 2007 9:42 PM
  • Folks,

    With the example above, I am getting Overflow exception?

     

    "The image data generated an overflow during processing"

     

    If you faced similar issue -could you please let me know - where the things may go wrong.

     

    Thanks And Regards,

    Anil

     

     

    Tuesday, November 13, 2007 12:59 PM
  • Hello ...

    I see that this is an old thread... but I've encountered the exact same problem with the "Cannot write to stream" issue. 

    I too spoke with Robert on getting my project to this point but the fact that there seems to be no reliable way to save the file is frustrating to say the least.

    I saw 2 other postings about this and the solutions the other guys came up with were too 1) put the call to save inside of a method that is called in it's own thread marked with STAThread and 2) to dump trying to make it work directy from c# and directly access the Windows Vista Shell API using C++. (you can read about it here - http://bloggingabout.net/blogs/erwyn/archive/2007/03/18/diving-into-c-for-flickr-metadata-synchr-v0-6-0-0.aspx)

    I need this to work from c# without the need to rely on the vista shell and/or c++.  I didn't try rewriting the save portion in a separate thread but will be trying that next unless someone has already got this figured out???

    cheers,
    Jon
    Monday, August 11, 2008 10:34 PM
  • Is Microsoft ever going to fully implement this stuff in .NET? This stuff should NOT be rocket science. It should be as easy as opening a file, setting a property, saving it, and closing it. Dealing with metadata was supposed to be one of the great new features in Vista. Supposedly Vista's UI was going to be 95%+ in .NET. It looks like Microsoft did not follow through on that. I hope they fix this for NETFX 4.0. I don't know why Microsoft doesn't let COM die the death that it deserves.
    Saturday, December 13, 2008 6:04 AM
  • Hello. I incurred with the same problems described here.

    First, something I tried was the in-place metadata writer, which works fine. The kind of problem everyone here had, I sorted out by calling the TrySave method before each and every SetQuery, not just before the first time.

    That only sorts out part of it, if the file does not have enough space for the metadata the SetQuery will fail. I found a nice example that sorts that out and guarantees a lossless transcoding, here is it: http://blogs.msdn.com/rwlodarc/archive/2007/07/18/using-wpf-s-inplacebitmapmetadatawriter.aspx

    I hope this sorts it all out, it definitely worked for me.


    Cheers,
    Felipe.
    Thursday, February 12, 2009 11:27 AM
  •  I agree the documentation is confusing.  Here is what I've discovered and some code that works with jpg files.  I think you should be able to adapt it to other fields and other image file types.  Good Luck

    public ModifyJpgMetadata()  
    {  
        //  
        // get the path to some jpg file  
        //  
        string jpegPath = "C:\\users\\scott\\Pictures\\sample\\xxx.jpg";  
        string jpegDirectory = Path.GetDirectoryName(jpegPath);  
        string jpegFileName = Path.GetFileNameWithoutExtension(jpegPath);  
        string jpegExtension = ".jpg";  
     
        BitmapDecoder decoder = null;  
        BitmapFrame bitmapFrame = null;  
        BitmapMetadata metadata = null;  
        if (File.Exists(jpegPath))  
        {  
            //  
            // load the jpg file with a JpegBitmapDecoder  
            //  
            using (Stream jpegStreamIn = File.Open(jpegPath, 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)  
            {  
                //  
                // now get an InPlaceBitmapMetadataWriter, modify the metadata and try to save  
                //  
                InPlaceBitmapMetadataWriter writer = bitmapFrame.CreateInPlaceBitmapMetadataWriter();  
                writer.SetQuery("/app1/ifd/exif:{uint=306}", "2001:01:01 01:01:01");  
                if (!writer.TrySave() == true)  
                {  
                    //  
                    // the size of the metadata has been increased and we can't save it  
                    //   
                    uint padding = 2048;  
     
                    BitmapMetadata metaData = (BitmapMetadata)bitmapFrame.Metadata.Clone();  
                    if (metaData != null)  
                    {  
                        //  
                        // Add padding  
                        //  
                        metaData.SetQuery("/app1/ifd/PaddingSchema:Padding", padding);  
                        //  
                        // modify the metadata  
                        metaData.SetQuery("/app1/ifd/exif:{uint=36867}", "2003:03:03 03:04:03");  
                        metaData.SetQuery("/app1/ifd/exif:{uint=306}", "2001:01:01 01:01:01");  
                        metaData.SetQuery("/app1/ifd/exif:{uint=36868}", "2002:02:02 02:02:02");  
                        //  
                        // get an encoder to create a new jpg file with the addit'l metadata.  
                        //  
                        JpegBitmapEncoder encoder = new JpegBitmapEncoder();  
                        encoder.Frames.Add(BitmapFrame.Create(bitmapFrame, bitmapFrame.Thumbnail, metaData, bitmapFrame.ColorContexts));  
                        string jpegNewFileName = Path.Combine(jpegDirectory, "JpegTemp.jpg");  
                        using (Stream jpegStreamOut = File.Open(jpegNewFileName, FileMode.CreateNew, FileAccess.ReadWrite))  
                        {  
                            encoder.Save(jpegStreamOut);  
                        }  
                        //  
                        // see if the metadata was really changed  
                        //  
                        using (Stream jpegStreamIn = File.Open(jpegNewFileName, FileMode.Open, FileAccess.ReadWrite))  
                        {  
                            decoder = new JpegBitmapDecoder(jpegStreamIn, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);  
                            BitmapFrame frame = decoder.Frames[0];  
                            BitmapMetadata bmd = (BitmapMetadata)frame.Metadata;  
                            string a1 = (string)bmd.GetQuery("/app1/ifd/exif:{uint=36867}");  
                            string a2 = (string)bmd.GetQuery("/app1/ifd/exif:{uint=306}");  
                            string a3 = (string)bmd.GetQuery("/app1/ifd/exif:{uint=36868}");  
                        }  
                    }  
                }  
            }  
        }  
    }  
     
    You can step through this code and look at a1, a2 and a3 to verify they have been changed.

    Just a note:
    The documentation shows code like this:
    Stream pngStream = new System.IO.FileStream("smiley.png", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);  
    PngBitmapDecoder pngDecoder = new PngBitmapDecoder(pngStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);  
    BitmapFrame pngFrame = pngDecoder.Frames[0];  
    InPlaceBitmapMetadataWriter pngInplace = pngFrame.CreateInPlaceBitmapMetadataWriter();  
    if (pngInplace.TrySave() == true)  
    { pngInplace.SetQuery("/Text/Description", "Have a nice day."); }  
    pngStream.Close(); 
    Which can't work because pngInplace.TrySave() always returns true.  This makes sense because you haven't changed the metadata size.
    The code should really look more like:
    Stream pngStream = new System.IO.FileStream("smiley.png", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);  
    PngBitmapDecoder pngDecoder = new PngBitmapDecoder(pngStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);  
    BitmapFrame pngFrame = pngDecoder.Frames[0];  
    InPlaceBitmapMetadataWriter pngInplace = pngFrame.CreateInPlaceBitmapMetadataWriter();  
     
    pngInplace.SetQuery("/Text/Description", "Have a nice day.");  
       
    if (pngInplace.TrySave() == true)  
    {   
       //  
       // add padding, change the metadata and save to a new file  
       //  
    }  
    pngStream.Close(); 
    Monday, March 2, 2009 12:16 AM
  • Much of this thread, although not all of it, involves accessing a jpg's metadata in a way that you can modify it.  The simplest way to do this is to use:

    BitmapFrame bitmapFrame = DecodeImage(imagePath);

    BitmapMetadata metadata = (BitmapMetadata)bitmapFrame.Metadata.Clone();

     

     

     

     

    This gives you a BitmapMetadata object that is a copy of the original but is not frozen and can be modified.

    Note: DecodeImage() is simply a function that determines what kind of image file it is and gets the appropriate decoder and decodes the image file into a BitmapFrame object.
    Friday, September 4, 2009 5:35 PM
  •  

    I got the 0x88982F71 and "Cannot write to the stream" when I tried to call the jpeg bitmap encoder's Save() function from a BackgroundWorker thread.

    void
    UpdateImageTagsThread_DoWork(object sender, DoWorkEventArgs e)

    {

        UpdateTagsEntry entry = null;

        while (true && _cancel == false)

        {

        while (_updateTagsQueue.Count > 0)

        {

            lock (_updateTagsQueue)

            {

                entry = _updateTagsQueue.Dequeue();

            }

            try

            {

                SaveTagsToImageEntry(entry);

            }

            catch (Exception excptn)

            {

                Console.WriteLine("Exception in SaveTagsToImageEntry() - {0}", excptn.Message);

            }

        }

        Thread.Sleep(100);

        }

    }

    When I changed the call to SaveTagsToImageEntry(entry); to:
     this.Dispatcher.Invoke(new this.Dispatcher(UpdateImageTagsCallback(SaveTagsToImageEntry), entry);

    The error went away.

    But it means I'm doing the encoding in the UI thread not in a background thread which was my intent.  I would dearly love to put this processing in a background thread.  Any ideas?

    Friday, September 4, 2009 6:04 PM