Subscribe to StCroixSkipper's Blog        RSS Feed
-----

Updating jpeg Metadata Wthout Loss of Quality

Icon Leave Comment
I've been sidetracked lately, trying to figure out how 'InPlaceBitmapMetadataWriter' is supposed to work. Roger Wlodarczyk has an entry on his blog that describes how it is supposed to work and that has helped. In my humble opinion, the documentation for 'InPlaceBitmapMetadataWriter' is particularly inadequate. Robert has provided some code to help.

I've written my code to initially attempt to update the jpeg metadata using 'InPlaceBitmapMetadataWriter'. If that fails, then fall back to adding additional padding to the metadata and trying again. The problem is that when you call 'InPlaceBitmapMetadataWriter.TrySave()' it seems to corrupt the file if there is not enough padding in place. Consequently, you need to make the attempt on a temporary file. Then if it fails, you add the metadata padding and try again.

I needed to perform the metadata update in a separate thread and ran into problems of image caching and figuring out the right set of flags to allow me to open and read/write image files in non-ui threads. That combined with the flags that tell the encoders not to re-encode the image data, just add padding and just update the metadata was a trial and error process.

Here is the code I've written to accomplish updating metadata on jpeg files without losing quality. Notice that at the beginning and end I create a 'FileInfo' object for the image and you can see that the length either doesn't change or it changes by the amount of padding you've added. ProcessJpgVile() simply calls 'UpdateMetadataInPlace()' and if 'UpdateMetadataInPlace()' fails it calls 'AddJpgMetadataPadding()' to pad the metadata before calling 'UpdateMetadataInPlace()' again.

If anyone wants to chime in, add constructive criticism, point out flaws or suggest better solutions or simply comment, please feel free to contribute.

        private void ProcessJpgFile(Photo photo)
        {
            //
            // Per Robert Wlodarczyk, if the TrySave function fails, some of the data may have been written out to 
            // output stream corrupting the output file.  You should/must try to attempt the TrySave() function on a 
            // temporary file rather than the original file to avoid this corruption.
            //
            FileInfo jpgFileInfoOriginal = new FileInfo(photo.FullPath);
            //
            // Attempt to write the new metadata with an inplace metadata writer
            //
            if (UpdateMetadataInPlace(photo) != true)
            {
                //
                // inplace update failed, now we need to add padding and try again.
                // 
                if (AddJpgMetadataPadding(photo.FullPath) == true)
                {
                    UpdateMetadataInPlace(photo);
                }
            }

            FileInfo jpgFileInfoFinal = new FileInfo(photo.FullPath);
        }



Here are the other two routines.
        private bool UpdateMetadataInPlace(Photo photo)
        {
            FileInfo jpgFileInfoOriginal = new FileInfo(photo.FullPath);

            bool bSuccess = false;

            string jpgImagePath = photo.FullPath;
            string directoryName = System.IO.Path.GetDirectoryName(jpgImagePath);
            string fileName = System.IO.Path.GetFileName(jpgImagePath);
            string tempPath = System.IO.Path.Combine(directoryName, string.Concat("temp_", fileName));
            //
            // copy original file to temp file because in place metadata update may corrupt file.
            //
            if (File.Exists(tempPath))
            {
                File.Delete(tempPath);
            }
            File.Copy(jpgImagePath, tempPath);

            using (Stream jpgFileStream = File.Open(tempPath, FileMode.Open, FileAccess.ReadWrite))
            {
                //
                // BitmapCacheOption.None tells the decoder to delay decoding until encoding. The JPEG 
                // encoder understands that the input was a JPEG and just copies over the image bits without decompressing 
                // and recompressing them to perform a lossless operation.
                //
                BitmapDecoder jpgDecoder = BitmapDecoder.Create(jpgFileStream, BitmapCreateOptions.None, BitmapCacheOption.None);

                if (jpgDecoder.Frames[0] != null && jpgDecoder.Frames[0].Metadata != null)
                {
                    InPlaceBitmapMetadataWriter metadataJpgWriter = jpgDecoder.Frames[0].CreateInPlaceBitmapMetadataWriter();
                    //
                    // now, we update the metadata.
                    //
                    UpdateImageMetadata(metadataJpgWriter, photo);
                    //
                    // try to save the metadata with the image
                    //
                    if (metadataJpgWriter.TrySave())
                    {
                        //
                        // TrySave() worked, set the return true
                        //
                        bSuccess = true;
                    }
                }
            }
            if (bSuccess == true)
            {
                //
                // TrySave() worked, copy the updated file to the original file
                // in this case I think a FileDelete()/File.Move() is faster.
                File.Delete(jpgImagePath);
                File.Move(tempPath, jpgImagePath);
            }
            else
            {
                //
                // TrySave() failed, cleanup by deleting the temporary file.
                //
                File.Delete(tempPath);
            }

            FileInfo jpgFileInfoFinal = new FileInfo(photo.FullPath);

            return bSuccess;
        }


        private bool AddJpgMetadataPadding(string jpgImagePath)
        {
            FileInfo jpgFileInfoOriginal = new FileInfo(jpgImagePath);

            bool bSuccess = false;

            string directoryName = System.IO.Path.GetDirectoryName(jpgImagePath);
            string fileName = System.IO.Path.GetFileName(jpgImagePath);
            string tempPath = System.IO.Path.Combine(directoryName, string.Concat("temp_", fileName));
            //
            // These BitmapCreateOptions inform the JPEG decoder and encoder that we're doing a 
            // lossless transcode operation.  Basically BitmapCreateOptions.PreservePixelFormat or'ed with 
            // BitmapCreateOptions.IgnoreColorProfile tell the decoder to use the original image bits
            //
            BitmapCreateOptions createOptions = BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile;
            //
            // padding the metadata area of the jpg with an additional 2K bytes should handle most situations.
            //
            uint paddingAmount = 4096; // 4Kb padding for modifying/adding metadata fields

            using (Stream fileStream = File.Open(jpgImagePath, FileMode.Open, FileAccess.Read))
            {
                BitmapDecoder jpgDecoder = BitmapDecoder.Create(fileStream, createOptions, BitmapCacheOption.None);
                //
                // check to make sure this is a jpeg file.
                //
                if (jpgDecoder.CodecInfo.FileExtensions.Contains("jpg"))
                {
                    JpegBitmapEncoder jpgEncoder = new JpegBitmapEncoder();
                    if (jpgDecoder.Frames[0] != null)
                    {
                        BitmapMetadata metadata;
                        // 
                        // Check to see that the image's metadata is not null
                        //     
                        if (jpgDecoder.Frames[0].Metadata == null)
                        {
                            metadata = new BitmapMetadata("jpeg");
                        }
                        else
                        {
                            metadata = jpgDecoder.Frames[0].Metadata.Clone() as BitmapMetadata;
                        }

                        // 
                        // since I only update the xmp metadata I only need to add padding to the xmp metadata.
                        //
                        metadata.SetQuery("/app1/ifd/PaddingSchema:Padding", paddingAmount);
                        metadata.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", paddingAmount);
                        metadata.SetQuery("/xmp/PaddingSchema:Padding", paddingAmount);

                        // Create a new frame identical to the one from the original image, except the metadata will have padding.
                        // Essentially we want to keep this as close as possible to:
                        //     output.Frames = original.Frames;
                        jpgEncoder.Frames.Add(BitmapFrame.Create(jpgDecoder.Frames[0], jpgDecoder.Frames[0].Thumbnail, metadata, jpgDecoder.Frames[0].ColorContexts));
                        using (Stream tempStream = File.Open(tempPath, FileMode.Create, FileAccess.ReadWrite))
                        {
                            jpgEncoder.Save(tempStream);
                        }
                        bSuccess = true;
                    }
                }
            }
            if (bSuccess == true)
            {
                //
                // copy image file with expanded padding to the original file
                // in this case I think a FileDelete()/File.Move() is faster.
                File.Delete(jpgImagePath);
                File.Move(tempPath, jpgImagePath);
            }

            FileInfo jpgFileInfoFinal = new FileInfo(jpgImagePath);

            return bSuccess;
        }



Then there are these two functions that actually modify the 'BitmapMetadata' object.
        private void UpdateImageMetadata(InPlaceBitmapMetadataWriter metadataJpgWriter, Photo photo)
        {
            if (!string.IsNullOrEmpty(photo.Comment))
            {
                metadataJpgWriter.Comment = photo.Comment;
            }
            if (!string.IsNullOrEmpty(photo.Title))
            {
                metadataJpgWriter.Title = photo.Title;
            }
            if (photo.DateTaken != null)
            {
                metadataJpgWriter.DateTaken = photo.DateTaken.ToShortDateString() + " " + photo.DateTaken.ToShortTimeString();
            }
            photo.Keywords.Sort();
            metadataJpgWriter.Keywords = new System.Collections.ObjectModel.ReadOnlyCollection<string>(photo.Keywords);
            if (!string.IsNullOrEmpty(photo.Subject))
            {
                metadataJpgWriter.Subject = photo.Subject;
            }
            if (!string.IsNullOrEmpty(photo.ApplicationName))
            {
                metadataJpgWriter.ApplicationName = photo.ApplicationName;
            }
            if (!string.IsNullOrEmpty(photo.Author))
            {
                string[] authors = photo.Author.Split(';');
                metadataJpgWriter.Author = new System.Collections.ObjectModel.ReadOnlyCollection<string>(authors);
            }
            if (!string.IsNullOrEmpty(photo.Copyright))
            {
                metadataJpgWriter.Copyright = photo.Copyright;
            }
            metadataJpgWriter.Rating = photo.Rating;
        }

        private void UpdateImageMetadata(BitmapMetadata metadata, Photo photo)
        {
            if (!string.IsNullOrEmpty(photo.Comment))
            {
                metadata.Comment = photo.Comment;
            }
            if (!string.IsNullOrEmpty(photo.Title))
            {
                metadata.Title = photo.Title;
            }
            if (photo.DateTaken != null)
            {
                metadata.DateTaken = photo.DateTaken.ToShortDateString() + " " + photo.DateTaken.ToShortTimeString();
            }
            photo.Keywords.Sort();
            metadata.Keywords = new System.Collections.ObjectModel.ReadOnlyCollection<string>(photo.Keywords);
            if (!string.IsNullOrEmpty(photo.Subject))
            {
                metadata.Subject = photo.Subject;
            }
            if (!string.IsNullOrEmpty(photo.ApplicationName))
            {
                metadata.ApplicationName = photo.ApplicationName;
            }
            if (!string.IsNullOrEmpty(photo.Author))
            {
                string[] authors = photo.Author.Split(';');
                metadata.Author = new System.Collections.ObjectModel.ReadOnlyCollection<string>(authors);
            }
            if (!string.IsNullOrEmpty(photo.Copyright))
            {
                metadata.Copyright = photo.Copyright;
            }
            metadata.Rating = photo.Rating;
        }



All of these functions use a Photo object and here is the code for the Photo class.
using System;
using System.IO;
using System.Collections.Generic;
using System.Windows.Media.Imaging;

using System.Security.Cryptography;
using System.Text;

namespace OverExposure
{
    public class Photo
    {
        #region trivialWords...

        private static List<string> _trivialWords = null;

        #endregion

        #region properties

        private bool _modifiedMetadata = false;
        public bool ModifiedMetadata
        {
            get { return _modifiedMetadata; }
            set { _modifiedMetadata = value; }
        }

        private string _name;
        public string Name
        {
            get { return _name; }
            set { _name = value.Trim(); }
        }

        private DateTime _dateTaken;
        public DateTime DateTaken
        {
            get { return _dateTaken; }
        }

        private string _size;
        public string Size
        {
            get { return _size; }
        }

        private string _path;
        public string Path
        {
            get { return _path; }
            set { _path = value.Trim(); }
        }

        public string FullPath
        {
            get { return System.IO.Path.Combine(_path, _name); }
        }

        private string _title;
        public string Title
        {
            get { return _title; }
        }

        private string _subject;
        public string Subject
        {
            get { return _subject; }
        }

        private string _comment;
        public string Comment
        {
            get { return _comment; }
        }

        private List<string> _keywords;
        public List<string> Keywords
        {
            get { return _keywords; }
        }

        private string _applicationName;
        public string ApplicationName
        {
            get { return _applicationName; }
        }

        private string _author;
        public string Author
        {
            get { return _author; }
        }

        private string _cameraManufacturer;
        public string CameraManufacturer
        {
            get { return _cameraManufacturer; }
        }

        private string _cameraModel;
        public string CameraModel
        {
            get { return _cameraModel; }
        }

        private string _copyright;
        public string Copyright
        {
            get { return _copyright; }
        }

        private string _format;
        public string Format
        {
            get { return _format; }
            set { _format = value.Trim(); }
        }

        private int _rating;
        public int Rating
        {
            get { return _rating; }
        }

        private BitmapImage _bitmapImage = null;
        public BitmapImage ImageBitmap
        {
            get { return _bitmapImage; }
            set { _bitmapImage = value; }
        }

        private object _treeViewItem = null;
        public object TvItem
        {
            get { return _treeViewItem; }
            set { _treeViewItem = value; }
        }

        #endregion

        #region constructor(s)

        public Photo(string filename)
        {
            if (_trivialWords == null)
            {
                AddTrivialWords();
            }
            FileInfo info = new FileInfo(filename);
            if (info.IsReadOnly)
            {
                try
                {
                    info.IsReadOnly = false;
                }
                catch(Exception){}
            }
            _size = (info.Length / 1024).ToString("N0") + " KB";
            _dateTaken = info.CreationTime;
            _name = info.Name;
            _path = info.DirectoryName;

            _keywords = new List<string>();

            //
            // always add in the directory name as the first keyword
            //
            string folderName = System.IO.Path.GetFileNameWithoutExtension(info.DirectoryName);
            
            //
            // the rest of the properties are null until they are set
            //
        }

        private void AddTrivialWords()
        {
            //
            // since all words 2 characters or less are considered trivial, only
            // words of 3 or more letters need be in this list
            //
            _trivialWords = new List<string>(50);
            lock (_trivialWords)
            {
                _trivialWords.Add("about");
                _trivialWords.Add("almost");
                _trivialWords.Add("always");
                _trivialWords.Add("and");
                _trivialWords.Add("any");
                _trivialWords.Add("are");
                _trivialWords.Add("but");
                _trivialWords.Add("can");
                _trivialWords.Add("did");
                _trivialWords.Add("else");
                _trivialWords.Add("even");
                _trivialWords.Add("for");
                _trivialWords.Add("get");
                _trivialWords.Add("got");
                _trivialWords.Add("had");
                _trivialWords.Add("her");
                _trivialWords.Add("him");
                _trivialWords.Add("his");
                _trivialWords.Add("how");
                _trivialWords.Add("into");
                _trivialWords.Add("its");
                _trivialWords.Add("just");
                _trivialWords.Add("not");
                _trivialWords.Add("non");
                _trivialWords.Add("our");
                _trivialWords.Add("per");
                _trivialWords.Add("put");
                _trivialWords.Add("she");
                _trivialWords.Add("such");
                _trivialWords.Add("than");
                _trivialWords.Add("that");
                _trivialWords.Add("the");
                _trivialWords.Add("thine");
                _trivialWords.Add("thou");
                _trivialWords.Add("thru");
                _trivialWords.Add("them");
                _trivialWords.Add("then");
                _trivialWords.Add("their");
                _trivialWords.Add("they");
                _trivialWords.Add("this");
                _trivialWords.Add("too");
                _trivialWords.Add("until");
                _trivialWords.Add("was");
                _trivialWords.Add("were");
                _trivialWords.Add("what");
                _trivialWords.Add("when");
                _trivialWords.Add("where");
                _trivialWords.Add("with");
                _trivialWords.Add("who");
                _trivialWords.Add("whose");
                _trivialWords.Add("why");
                _trivialWords.Add("yet");
                _trivialWords.Add("you");
                _trivialWords.Add("your");
                _trivialWords.Add("photo");
                _trivialWords.Add("image");
                _trivialWords.Add("pictures");
                _trivialWords.Add("photos");
                _trivialWords.Add("images");
                _trivialWords.Add("pictures");
                _trivialWords.Add("photograph");
                _trivialWords.Add("photographs");
                _trivialWords.Add("bmp");
                _trivialWords.Add("jpg");
                _trivialWords.Add("jpeg");
                _trivialWords.Add("gif");
                _trivialWords.Add("tif");
                _trivialWords.Add("copy");
                _trivialWords.Add("available");
                _trivialWords.Add("not available");
            }
        }

        public bool IsBogusName(string filename)
        {
            bool bRtn = true;
            int count = 0;

            //
            // if there is are more than 3 letters in the name assume it is not bogus.
            //
            foreach (char c in filename)
            {
                if (char.IsLetter(c))
                {
                    count++;
                    if (count > 3)
                    {
                        bRtn = false;
                        break;
                    }
                }
            }
            return bRtn;
        }

        #endregion

        #region public member functions

        public override string ToString()
        {
            return FullPath;
        }

        public void AddKeyword(string keyword, bool setIsModified)
        {
            keyword = keyword.Trim();
            if (keyword.Length > 2)  // ignore words less than 3 characters in length.
            {
                if (!_trivialWords.Contains(keyword.ToLower()))
                {
                    if (isValidKeyword(keyword))
                    {
                        string lowercaseKeyword = keyword.ToLower();
                        if (!_keywords.Contains(keyword) && !_keywords.Contains(lowercaseKeyword))
                        {
                            _keywords.Add(keyword);
                            if (setIsModified)
                            {
                                _modifiedMetadata = true;
                            }
                        }
                    }
                }
                else
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void RemoveKeyword(string keyword)
        {
            keyword = keyword.Trim();
            string lowercaseKeyword = keyword.ToLower();
            if (_keywords.Contains(keyword))
            {
                _keywords.Remove(keyword);
                _modifiedMetadata = true;
            }
            else if (_keywords.Contains(lowercaseKeyword))
            {
                _keywords.Remove(keyword);
                _modifiedMetadata = true;
            }
        }

        public void UpdateDateTaken(DateTime dateTaken, bool setIsModified)
        {
            if (_dateTaken != dateTaken)
            {
                _dateTaken = dateTaken;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void UpdateTitle(string title, bool setIsModified)
        {
            title = title.Trim();
            if (0 != string.Compare(_title, title))
            {
                _title = title;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void UpdateSubject(string subject, bool setIsModified)
        {
            subject = subject.Trim();
            if (0 != string.Compare(_subject, subject))
            {
                _subject = subject;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void UpdateComment(string comment, bool setIsModified)
        {
            comment = comment.Trim();
            if (0 != string.Compare(_comment, comment))
            {
                _comment = comment;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void UpdateApplicationName(string applicationName, bool setIsModified)
        {
            applicationName = applicationName.Trim();
            if (0 != string.Compare(_applicationName, applicationName))
            {
                _applicationName = applicationName;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void UpdateAuthor(string author, bool setIsModified)
        {
            string[] parts = author.Split(';');
            author = parts[0].Trim();
            if (0 != string.Compare(_author, author))
            {
                _author = author;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void UpdateCameraManufacturer(string cameraManufacturer, bool setIsModified)
        {
            if (0 != string.Compare(_cameraManufacturer, cameraManufacturer))
            {
                _cameraManufacturer = cameraManufacturer;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void UpdateCameraModel(string cameraModel, bool setIsModified)
        {
            if (0 != string.Compare(_cameraModel, cameraModel))
            {
                _cameraModel = cameraModel;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void UpdateCopyright(string copyright, bool setIsModified)
        {
            copyright = copyright.Trim();
            if (0 != string.Compare(_copyright, copyright))
            {
                _copyright = copyright;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public void UpdateRating(int rating, bool setIsModified)
        {
            if (_rating != rating)
            {
                _rating = rating;
                if (setIsModified)
                {
                    _modifiedMetadata = true;
                }
            }
        }

        public static bool isValidKeyword(string keyword)
        {
            int result;
            bool bRtn = false;
            if (!string.IsNullOrEmpty(keyword) && keyword.Length > 2)
            {
                if (int.TryParse(keyword, out result))
                {
                    if (keyword.Length == 4)
                    {
                        bRtn = true;
                    }
                }
                else
                {
                    bRtn = true;
                }
            }
            return bRtn;
        }

        public void ClearKeywords()
        {
            _keywords.Clear();
        }

        #endregion

        #region graveyard
        /*
        internal void MD5(BitmapDecoder bd, bool bForce)
        {
            int index = -1;
            //
            // determine if an MD5 digest has been calculated for this image
            //
            foreach( string keywd in _keywords)
            {
                if(keywd.StartsWith("MD5"))
                {
                    index = _keywords.IndexOf(keywd);
                    break;
                }
            }

            string md5Digest;
            if(bForce)
            {
                //
                // if bForce is true, then remove the existing MD5 digest, then recalculate and add a new one
                //
                if(index > -1)
                {
                    _keywords.RemoveAt(index);
                    _keywords.Add(CalculateMD5Digest(bd));
                }
            }
            else
            {
                // 
                // if bForce is false, then only calculate a new MD5 digest if none exists
                //
                if (index == -1)
                {
                    _keywords.Add(CalculateMD5Digest(bd));
                }
            }
        }

        private string CalculateMD5Digest(BitmapDecoder bd)
        {
            StringBuilder sb = new StringBuilder("MD5");
            int width = bd.Frames[0].PixelWidth;
            int height = bd.Frames[0].PixelHeight;
            System.Windows.Media.PixelFormat pixelFmt = bd.Frames[0].Format;
            int stride = width * pixelFmt.BitsPerPixel;
            byte[] pixels = new byte[height * stride];
            bd.Frames[0].CopyPixels(pixels, stride, 0);
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] md5Digest = md5.ComputeHash(pixels);
            foreach(byte b in md5Digest)
            {
                sb.Append(b.ToString("x2"));
            }
            return sb.ToString();
        }
        */
        #endregion
    }
}



Maybe next week I can get back to 3D and Animation.

0 Comments On This Entry

 

Trackbacks for this entry [ Trackback URL ]

There are no Trackbacks for this entry

May 2019

S M T W T F S
   1234
567891011
12131415161718
19 202122232425
262728293031 

Tags

    Recent Entries

    Search My Blog

    0 user(s) viewing

    0 Guests
    0 member(s)
    0 anonymous member(s)

    Categories