/Meta.Core/ImageMeta/ImageProcessor.cs
https://bitbucket.org/muruge/meta-public · C# · 645 lines · 423 code · 125 blank · 97 comment · 52 complexity · 4dcc9110e16bf2b0663edc3a07033d2d MD5 · raw file
- using System;
- using System.Collections.Generic;
- using System.Collections.ObjectModel;
- using System.IO;
- using System.Linq;
- using System.Text;
- using System.Text.RegularExpressions;
- using System.Windows.Media.Imaging;
- using Meta.Core.Converters;
-
- namespace Meta.Core.ImageMeta
- {
- public class ImageProcessor : Processor<IImageProperties>, IImageProcessor
- {
- #region constructors
-
- public ImageProcessor(string path)
- :base(path)
- {
- }
-
- #endregion
-
- #region public methods
-
- #region static methods
-
- public static BitmapImage Load(string path)
- {
- var image = new BitmapImage();
- image.BeginInit();
- image.CacheOption = BitmapCacheOption.OnLoad; //cache the image into memory and close the actual image file.
- image.UriSource = new Uri(path);
- image.EndInit();
-
- return image;
- }
-
- /// <summary>
- /// Checks if the file is an image file.
- /// </summary>
- /// <param name="path">Full Path of the file</param>
- /// <returns>Boolean value indicating wheather or not the file is an image file.</returns>
- public static bool IsImageFile(string path)
- {
- var fileExtension = System.IO.Path.GetExtension(path);
- var imageFileExtensions = new[] { ".jpg", ".tif", ".tiff", ".jpeg", ".png", "gif", ".bmp", ".jfif", ".jpe" };
- bool isImageFile = false;
-
- if (imageFileExtensions
- .Any(imageFileExtension => imageFileExtension.Equals(fileExtension, StringComparison.CurrentCultureIgnoreCase)))
- isImageFile = true;
-
- return isImageFile;
- }
-
- /// <summary>
- /// Checks if the format of the image file is currently supported.
- /// </summary>
- /// <returns>Boolean value indicating wheather or not image format is supported.</returns>
- public static bool IsSupportedFormat(string path)
- {
- return new ImageProcessor(path)
- .IsSupportedFormat();
- }
-
- #endregion
-
- #region instance methods
-
- /// <summary>
- /// Writes the metadata properties into the image file.
- /// </summary>
- /// <param name="properties">Metadata properties to be written.</param>
- /// <returns>Boolean value indicating whether or not write succeeded.</returns>
- public override bool TryWrite(IImageProperties properties)
- {
- try
- {
- if (!IsPathValid())
- return IsWriteSuccessfull;
-
- //FileAccess.ReadWrite access required to overwrite the file with modified metadata
- using (Stream originalFileStream = File.Open(Path, FileMode.Open, FileAccess.ReadWrite))
- {
- //create a temporary file to save the image with new metadata
- string directory = System.IO.Path.GetDirectoryName(Path);
- string tempFile = "";
-
- //byte array to store the contents of image file
- var buffer = new byte[originalFileStream.Length];
-
- //byte array reads the contents of the stream
- originalFileStream.Read(buffer, 0, (int)originalFileStream.Length);
-
- //create a memory stream to work with the image.
- using (var memoryStream = new MemoryStream(buffer))
- {
- //use memmory stream to create the BitmapDecoder. This way we work only with the stream in
- //memory and release the lock in the original file, so it can be used by other processes.
- //BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile
- //is required to preserve the pixel format of the image
- //BitmapCacheOption.None says to wait until the decoding is complete. It doesnot cache the image
- //and accesses it for every request.
- BitmapDecoder decoder = BitmapDecoder.Create(memoryStream,
- BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile,
- BitmapCacheOption.None);
-
- #region Metadata processing
-
- if (decoder.Frames[0] != null && decoder.Frames[0].Metadata != null)
- {
- //metadata will be frozen. So clone it to allow modification
- var metaData = decoder.Frames[0].Metadata.Clone() as BitmapMetadata;
-
- // additional space added to image frame so it can embrace the new metadata.
- const uint paddingAmount = 5000;
-
- //padding can only exist only in IFD, EXIF and XMP formats
- if (metaData != null)
- {
- metaData.SetQuery("/app1/ifd/PaddingSchema:Padding", paddingAmount);
- metaData.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", paddingAmount);
- metaData.SetQuery("/xmp/PaddingSchema:Padding", paddingAmount);
- }
-
- string imgFormat = string.Empty;
- BitmapEncoder encoder = new JpegBitmapEncoder();
-
- if (decoder.CodecInfo != null)
-
- if (decoder.CodecInfo.FileExtensions.Contains("jpg"))
- {
- imgFormat = "JPEG";
- encoder = new JpegBitmapEncoder();
- tempFile = directory + @"\temp.jpg";
- }
- else if (decoder.CodecInfo.FileExtensions.Contains("tiff"))
- {
- imgFormat = "TIFF";
- encoder = new TiffBitmapEncoder();
- tempFile = directory + @"\temp.tiff";
- }
-
- var iptc = new IptcStandard(imgFormat);
- var xmp = new XmpStandard(imgFormat);
- var exif = new ExifStandard(imgFormat);
- var ifd = new IfdStandard(imgFormat);
-
- //assign new metadata value
- #region Values Assignment
-
- //Title
- ProcessProperty(ref metaData, properties.Title, "System.Title", new[] { iptc.ObjectName, ifd.Title });
-
- //Author
- if (string.IsNullOrEmpty(properties.Authors))
- DeletePathIfExist();
- else
- {
- IList<string> authorsList = Utility.GetListItems(properties.Authors);
-
- if (authorsList.Any())
- {
- CreatePathIfNotExist(ref metaData, new[] { iptc.ByLine, ifd.Authors });
- var auth = new ReadOnlyCollection<string>(authorsList);
-
- metaData.Author = auth;
-
- //removed because it causes some encoding error.
- //metaData.SetQuery(exif.Artist, ASCIIEncoding.ASCII.GetBytes(this.Authors));
- }
- }
-
- //Subject
- ProcessProperty(ref metaData, properties.Subject, "System.Subject", new[] { ifd.Subject });
-
- //Explicit mapping to EXIF and IPTC equivalents.
- ProcessProperty(ref metaData, properties.Subject, new[] { xmp.Description, iptc.Headline, exif.ImageDescription });
-
- //Keywords
- if (string.IsNullOrEmpty(properties.Keywords))
- DeletePathIfExist();
- else
- {
- IList<string> keywordsList = Utility.GetListItems(properties.Keywords);
-
- if (keywordsList.Any())
- {
- CreatePathIfNotExist(ref metaData, new[] { iptc.Keywords });
- metaData.Keywords = new ReadOnlyCollection<string>(keywordsList);
-
- //Explicit write required because System.Keywords deletes IFD keyword path.
- //Unicode encoding required because datatype of values is unicode byte[].
- string keywords = keywordsList
- .Aggregate(string.Empty, (current, keyword) => current + (string.IsNullOrEmpty(current) ? keyword : Utility.Delimiter + keyword));
-
- metaData.SetQuery(ifd.Keywords, Encoding.Unicode.GetBytes(keywords.ToCharArray()));
- }
- }
-
- //Comments
- ProcessProperty(ref metaData, properties.Comments, "System.Comment", new[] { iptc.Caption, exif.UserComment, ifd.Comments });
-
- //Date
- DateTime date;
- if (DateTime.TryParse(properties.Date, out date))//to assign only if it is a valid date
- {
- metaData.DateTaken = date.ToShortDateString();
- // setting date using handler does write to exif though it is suppose to.
- //hence explicit writing is done for EXIF and IPTC
- metaData.SetQuery(exif.DateTimeOriginal, date.ToString("yyyy:MM:dd HH:mm:ss"));
- metaData.SetQuery(iptc.DateCreated, date.ToString("yyyyMMdd"));
- }
- else //removes all date paths if date does not has a valid value.
- {
- DeletePathIfExist();
- }
-
- //Park Code
- ProcessProperty(ref metaData, properties.City, new[] { iptc.City, xmp.City });
-
- //Access Constraint
- if (string.IsNullOrEmpty(properties.Urgency.ToString()))
- DeletePathIfExist();
- else
- {
- metaData.SetQuery(iptc.Urgency, (int)properties.Urgency);
- metaData.SetQuery(xmp.Urgency, ((int)properties.Urgency).ToString());
- }
-
- //Copyright Notice
- ProcessProperty(ref metaData, properties.CopyrightNotice, "System.Copyright", new[] { iptc.Copyright, ifd.Copyright });
-
- //Location Details
- ProcessProperty(ref metaData, properties.Location, new[] { iptc.SpecialInstructions, xmp.Instructions });
-
- //Contact Information
- ProcessProperty(ref metaData, properties.Contact, new[] { iptc.WriterEditor, xmp.CaptionWriter });
-
- var dmsConverter = new RationalToDmsConverter();
- var coordinates = new Coordinates();
- double latLong;
- string reference;
- double[] dms;
-
- //Latitude
- if (Double.TryParse(properties.GpsLatitude, out latLong) &&
- (Math.Abs(latLong) >= 0 && Math.Abs(latLong) <= 90))
- {
- dms = coordinates.ConvertToDms(Math.Abs(latLong));
- reference = latLong > 0D ? "N" : "S";
-
- metaData.SetQuery(exif.GpsLatitude, dmsConverter.ConvertBack(dms));
- metaData.SetQuery(exif.GpsLatitudeRef, reference);
- metaData.SetQuery(xmp.GpsLatitude, (
- dms[0] + "," +
- dms[1] + "," +
- dms[2] + reference));
- }
- else
- {
- DeletePathIfExist();
- }
-
- //Longitude
- if (Double.TryParse(properties.GpsLongitude, out latLong) &&
- (Math.Abs(latLong) >= 0 && Math.Abs(latLong) <= 180))
- {
- dms = coordinates.ConvertToDms(Math.Abs(latLong));
- reference = latLong < 0D ? "W" : "E";
- metaData.SetQuery(exif.GpsLongitude, dmsConverter.ConvertBack(dms));
- metaData.SetQuery(exif.GpsLongitudeRef, reference);
- metaData.SetQuery(xmp.GpsLongitude, (
- dms[0] + "," +
- dms[1] + "," +
- dms[2] + reference));
- }
- else
- {
- DeletePathIfExist();
- }
-
- //Datum
- ProcessProperty(ref metaData, properties.GpsDatum, new[] { exif.GpsMapDatum, xmp.GpsMapDatum });
-
- //Image Direction
- if (Double.TryParse(properties.GpsImgDirection, out latLong) &&
- (Math.Abs(latLong) >= 0 && Math.Abs(latLong) <= 359.99))
- {
- dms = coordinates.ConvertToDms(Math.Abs(latLong));
- metaData.SetQuery(exif.GpsImgDirection, dmsConverter.ConvertBack(dms));
- metaData.SetQuery(xmp.GpsImgDirection, dms[0].ToString());
- }
-
- //Image Direction Reference
- if (properties.GpsImgDirectionRef.Equals("T", StringComparison.CurrentCultureIgnoreCase) ||
- properties.GpsImgDirectionRef.Equals("M", StringComparison.CurrentCultureIgnoreCase))
- ProcessProperty(ref metaData, properties.GpsImgDirectionRef.ToUpper(), new[] { exif.GpsImgDirectionRef, xmp.GpsImgDirectionRef });
-
- #endregion Values Assignment
-
- BitmapFrame frame = BitmapFrame.Create(decoder.Frames[0], decoder.Frames[0].Thumbnail, metaData, decoder.Frames[0].ColorContexts);
-
- //adds the frame with new metadata to the encoder
- encoder.Frames.Add(frame);
-
- using (Stream tempFileStream = File.Open(tempFile, FileMode.Create, FileAccess.ReadWrite))
- {
- //saves the image to the temporary file's stream
- encoder.Save(tempFileStream);
- }
-
- //release the resources
- originalFileStream.Dispose(); //releases the lock on original file enabling to overwrite it
-
- //overwrite the original file with the contents of temp file
- File.Copy(tempFile, Path, true);
-
- //delete the temporary file
- File.Delete(tempFile);
- }
-
- #endregion Metadata processing
- }
- }
-
- ErrorMessage = string.Empty;
-
- //flag metadata has been written sucessfully
- IsWriteSuccessfull = true;
- }
- catch (Exception ex)
- {
- ErrorMessage = ex.Message;
-
- //flag error occured during process
- IsWriteSuccessfull = false;
- }
-
- return IsWriteSuccessfull;
- }
-
- /// <summary>
- /// Reads metadata properties from the image file.
- /// </summary>
- /// <param name="properties">IImageProperties instance to which metadata values have to be assigned.</param>
- /// <returns>Boolean value indicating whether or not read succeeded.</returns>
- public override bool TryRead(out IImageProperties properties)
- {
- try
- {
- if (!IsPathValid())
- {
- properties = null;
- return IsReadSuccessfull;
- }
-
- using (Stream imageFileStream = File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
- {
- //byte array to store the contents of image file
- var buffer = new byte[imageFileStream.Length];
-
- //read the imagefile into the byte array
- imageFileStream.Read(buffer, 0, (int)imageFileStream.Length);
-
- //create a memory stream with the bytes read
- using (var memoryStream = new MemoryStream(buffer))
- {
- BitmapSource image = BitmapFrame.Create(memoryStream);
- var metaData = (BitmapMetadata)image.Metadata;
-
- #region Read Metadata Properties
-
- string imageFormat = System.IO.Path.GetExtension(Path);
-
- //read properties into the corresponding ImageDocument properties
-
- const StringComparison compareOption = StringComparison.CurrentCultureIgnoreCase;
- string format = string.Empty;
-
- if (imageFormat != null)
- if (imageFormat.Equals(".jpg", compareOption) | imageFormat.Equals("jpeg", compareOption))
- format = "JPEG";
- else if (imageFormat.Equals(".tif", compareOption) | imageFormat.Equals("tiff", compareOption))
- format = "TIFF";
-
- var xmp = new XmpStandard(format);
- var iptc = new IptcStandard(format);
- var exif = new ExifStandard(format);
- var ifd = new IfdStandard(format);
-
- properties = new ImageProperties();
-
- if (metaData != null)
- {
- properties.Path = Path;
-
- properties.Title = metaData.Title;
- properties.Comments = metaData.Comment;
- properties.CopyrightNotice = metaData.Copyright;
-
- properties.Authors = GetListItemsAsString(metaData.Author);
- properties.Keywords = GetListItemsAsString(metaData.Keywords);
-
- properties.Subject = GetPrioritizedValue(metaData,
- new[]
- {
- xmp.Description, iptc.Headline, exif.ImageDescription,
- ifd.Subject
- }).ToString();
-
- properties.City = GetPrioritizedValue(metaData,
- new[] { xmp.City, iptc.City }).ToString();
- properties.Location = GetPrioritizedValue(metaData,
- new[] { xmp.Instructions, iptc.SpecialInstructions }).ToString();
- properties.Contact = GetPrioritizedValue(metaData,
- new[] { xmp.CaptionWriter, iptc.WriterEditor }).ToString();
-
-
- //convert date to short date format.
- //If conversion fails the value is shown as it is.
- DateTime dateTaken;
- properties.Date = DateTime.TryParse(metaData.DateTaken, out dateTaken)
- ? dateTaken.ToShortDateString()
- : metaData.DateTaken;
-
- string access = GetPrioritizedValue(metaData,
- new[] { xmp.Urgency, iptc.Urgency }).ToString();
- int accessLevel;
- accessLevel = Int32.TryParse(access, out accessLevel) ? accessLevel : 8;
- properties.Urgency = (Urgency)accessLevel;
-
- properties.GpsDatum = GetPrioritizedValue(metaData,
- new[] { exif.GpsMapDatum, xmp.GpsMapDatum }).ToString();
-
- properties.GpsLatitude = GetCoordinateValues(
- GetPrioritizedValue(metaData, new[] { exif.GpsLatitude }),
- GetPrioritizedValue(metaData, new[] { exif.GpsLatitudeRef }).ToString(),
- GetPrioritizedValue(metaData, new[] { xmp.GpsLatitude }));
-
- properties.GpsLongitude = GetCoordinateValues(
- GetPrioritizedValue(metaData, new[] { exif.GpsLongitude }),
- GetPrioritizedValue(metaData, new[] { exif.GpsLongitudeRef }).ToString(),
- GetPrioritizedValue(metaData, new[] { xmp.GpsLongitude }));
-
- properties.GpsImgDirection = ConvertRationalToDecimalDegrees(GetPrioritizedValue(metaData, new[] { exif.GpsImgDirection, xmp.GpsImgDirection })).ToString();
-
- if (!string.IsNullOrEmpty(properties.GpsImgDirection))
- properties.GpsImgDirection += Utility.DegreeSymbol;
-
- properties.GpsImgDirectionRef = GetPrioritizedValue(metaData, new[] { exif.GpsImgDirectionRef, xmp.GpsImgDirectionRef }).ToString();
- }
-
- #endregion Read Metadata Properties
- }
- }
-
- ErrorMessage = string.Empty;
-
- //flag metadata has been read sucessfully
- IsReadSuccessfull = true;
- }
- catch (Exception ex)
- {
- properties = null;
- ErrorMessage = ex.Message;
-
- //flag error occured while reading
- IsReadSuccessfull = false;
- }
-
- return IsReadSuccessfull;
- }
-
- /// <summary>
- /// Checks if the format of the image file is currently supported.
- /// </summary>
- /// <returns>Boolean value indicating wheather or not image format is supported.</returns>
- public bool IsSupportedFormat()
- {
- var supportedFormats = new List<string> { ".jpg", ".jpeg", ".tiff", ".tif" };
- return IsSupportedFormat(supportedFormats);
- }
-
- #endregion
-
- #endregion public methods
-
- #region private methods
-
- /// <summary>
- /// Gets the prioritized property value based on the Photo Metadata Policy.
- /// </summary>
- /// <param name="metaData">Instance of metadata</param>
- /// <param name="queries">Queries from which to get value. Must be arranged in their order of precedence.</param>
- /// <returns></returns>
- private static object GetPrioritizedValue(BitmapMetadata metaData, IEnumerable<string> queries)
- {
- //Rule for reading a property value. This is inline with Microsoft's Photo Metadata Policy.
- //Order of precedence(based on their existence) : XMP , IPTC, EXIF, IFD
- //If XMP exists it is returned. If not it looks for IPTC and returns if it exists. If not looks for EXIF and then IFD.
- object prioritizedValue = string.Empty;
-
- foreach (string t in queries.Where(metaData.ContainsQuery))
- {
- prioritizedValue = metaData.GetQuery(t);
- break;
- }
-
- //Properties modified using XP summary properties are stored as byte arrays in IFD.
- if (prioritizedValue is byte[])
- prioritizedValue = Encoding.ASCII.GetString((byte[])prioritizedValue).Trim('\0');
- else if (prioritizedValue is BitmapMetadataBlob)//EXIF softwares write some properties as BitmapMetadataBlob
- prioritizedValue = Encoding.UTF8.GetString(((BitmapMetadataBlob)prioritizedValue).GetBlobValue()).Trim('\0');
-
- //else if (prioritizedValue is List<)
- return prioritizedValue;
- }
-
- private static string GetListItemsAsString(IEnumerable<string> list)
- {
- string stringValue = string.Empty;
-
- if (list != null)
- stringValue = list
- .Aggregate(stringValue, (current, item) => current + (string.IsNullOrEmpty(item) ? string.Empty : item + ";"));
-
- return stringValue;
- }
-
- private static double ConvertRationalToDecimalDegrees(object value)
- {
- if (value is ulong[])
- {
- var converter = new RationalToDmsConverter();
- var result = (double)converter.Convert(value);
- return result;
- }
-
- return 0;
- }
-
- private static string GetCoordinateValues(object exifLatLong, string exifRef, object xmpLatLong)
- {
- const StringComparison compareOption = StringComparison.CurrentCultureIgnoreCase;
- double reference, latLong;
-
- if (exifLatLong is ulong[])
- {
- reference = ((exifRef.Equals("N", compareOption)) || (exifRef.Equals("E", compareOption)))
- ? 1 : -1;
- latLong = ConvertRationalToDecimalDegrees(exifLatLong);
- latLong = latLong * reference;
- return latLong + Utility.DegreeSymbol;
- }
-
- if (!string.IsNullOrEmpty(xmpLatLong.ToString()))
- {
- //Matches DDD,MM,SSk or DDD,MM.mmk pattern where
- //DDD is a number of degrees,MM is a number of minutes,SS is a number of seconds
- //mm is a fraction of minutes,k is a single character N, S, E, or W indicating a direction (north, south, east, west)
- const string pattern = @"^[0-9]{1,3},[0-9]{1,2}(,[0-9]{1,2}|\.[0-9]+)[NnSsEeWw]$";
-
- if (Regex.IsMatch(xmpLatLong.ToString(), pattern))
- {
- var coordinates = new Coordinates();
- string[] values = xmpLatLong.ToString().Split(new[] { ',' });
- string lastWord = values[values.Length - 1];
- string rfrnc = lastWord.Substring(lastWord.Length - 1);
-
- values[values.Length - 1] = lastWord.TrimEnd(rfrnc.ToCharArray());
- var latLongD = new double[values.Length];
-
- for (int i = 0; i < values.Length; i++)
- {
- latLongD[i] = Double.Parse(values[i]);
- }
-
- reference = ((rfrnc.Equals("N", compareOption)) || (rfrnc.Equals("E", compareOption)))
- ? 1 : -1;
-
- latLong = coordinates.ConvertToDecimalDegrees(latLongD) * reference;
- return latLong + Utility.DegreeSymbol;
- }
-
- return xmpLatLong.ToString();
- }
-
- if (!string.IsNullOrEmpty(exifLatLong.ToString()))
- return exifLatLong + exifRef;
-
- return string.Empty;
- }
-
- private static void CreatePathIfNotExist(ref BitmapMetadata metaData, IEnumerable<string> paths)
- {
- foreach (string path in paths)
- {
- if (!metaData.ContainsQuery(path))
- metaData.SetQuery(path, string.Empty);
- }
- }
-
- private static void DeletePathIfExist()
- {
- return;
-
- //commented on 12/5/2010
- //not required to delete paths that donot have values.
-
- //foreach (string path in paths)
- //{
- // if (metaData.ContainsQuery(path))
- // metaData.RemoveQuery(path);
- //}
- }
-
- private static void ProcessProperty(ref BitmapMetadata metaData, string propertyValue, IEnumerable<string> paths)
- {
- if (string.IsNullOrEmpty(propertyValue))
- DeletePathIfExist();
- else
- {
- foreach (string path in paths)
- {
- metaData.SetQuery(path, propertyValue);
- }
- }
- }
-
- private static void ProcessProperty(ref BitmapMetadata metaData, string propertyValue, string property, IEnumerable<string> paths)
- {
- if (string.IsNullOrEmpty(propertyValue))
- DeletePathIfExist();
- else
- {
- CreatePathIfNotExist(ref metaData, paths);
- metaData.SetQuery(property, propertyValue);
- }
- }
-
- #endregion private methods
- }
- }