Click here to Skip to main content
15,886,362 members
Articles / Web Development / HTML

Gallery Server Pro - An ASP.NET Gallery for Sharing Photos, Video, Audio and Other Media

Rate me:
Please Sign up or sign in to vote.
4.86/5 (131 votes)
18 Oct 2013GPL331 min read 825.4K   539  
Gallery Server Pro is a complete, stable ASP.NET gallery for sharing photos, video, audio and other media. This article presents the overall architecture and major features.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows.Media.Imaging;
using GalleryServerPro.Business.Interfaces;
using GalleryServerPro.Business.Metadata;
using GalleryServerPro.Business.Wpf.Properties;
using System.IO;

namespace GalleryServerPro.Business.Wpf
{
	/// <summary>
	/// Contains functionality for extracting image metadata using the .NET 3.0 Windows Presentation Foundation (WPF) classes.
	/// </summary>
	public static class WpfMetadataExtractor
	{
		private const string DateTimeFormatStringForMetadata = "ddd, dd MMM yyyy h:mm:ss tt";
		private const string IptcQueryFormatString = "/app13/irb/8bimiptc/iptc/{{str={0}}}";

		/// <summary>
		/// Add items to the specified collection from metadata accessed through the Windows Presentation Foundation (WPF)
		/// classes. This includes the following items: Title, Author, Data taken, Camera model, Camera manufacturer, Keywords,
		/// Rating, Comment, Copyright, Subject. If any of these items are null, they are not added.
		/// </summary>
		/// <param name="imageFilePath">The image file path.</param>
		/// <param name="metadataItems">The collection of <see cref="IGalleryObjectMetadataItem" /> objects to add to.</param>
		/// <exception cref="System.Security.SecurityException">This function requires running under Full Trust, and will
		/// throw a security exception if it doesn't have it.</exception>
		public static void AddWpfBitmapMetadata(string imageFilePath, IGalleryObjectMetadataItemCollection metadataItems)
		{
			BitmapMetadata bmpMetadata = GetBitmapMetadata(imageFilePath);

			if (bmpMetadata != null)
			{
				AddGpsMetadata(bmpMetadata, metadataItems);

				AddIptcMetadata(bmpMetadata, metadataItems);

				try
				{
					if ((bmpMetadata.Title != null) && (!String.IsNullOrEmpty(bmpMetadata.Title.Trim())))
						metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.Title, Resources.Metadata_Title, bmpMetadata.Title.Trim(), true);

					if (bmpMetadata.Author != null)
						metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.Author, Resources.Metadata_Author, ConvertStringCollectionToDelimitedString(bmpMetadata.Author), true);

					if (bmpMetadata.DateTaken != null)
					{
						DateTime dateTaken = TryParseDate(bmpMetadata.DateTaken);
						if (dateTaken.Year > DateTime.MinValue.Year)
						{
							metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.DatePictureTaken, Resources.Metadata_DatePictureTaken, dateTaken.ToString(DateTimeFormatStringForMetadata, CultureInfo.InvariantCulture), true);
						}
					}

					if ((bmpMetadata.CameraModel != null) && (!String.IsNullOrEmpty(bmpMetadata.CameraModel.Trim())))
						metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.CameraModel, Resources.Metadata_CameraModel, bmpMetadata.CameraModel.Trim(), true);

					if ((bmpMetadata.CameraManufacturer != null) && (!String.IsNullOrEmpty(bmpMetadata.CameraManufacturer.Trim())))
						metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.EquipmentManufacturer, Resources.Metadata_EquipmentManufacturer, bmpMetadata.CameraManufacturer.Trim(), true);

					if (bmpMetadata.Keywords != null)
						metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.Keywords, Resources.Metadata_Keywords, ConvertStringCollectionToDelimitedString(bmpMetadata.Keywords), true);

					if (bmpMetadata.Rating > 0)
					metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.Rating, Resources.Metadata_Rating, bmpMetadata.Rating.ToString(CultureInfo.InvariantCulture), true);

					if ((bmpMetadata.Comment != null) && (!String.IsNullOrEmpty(bmpMetadata.Comment.Trim())))
						metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.Comment, Resources.Metadata_Comment, bmpMetadata.Comment.Trim(), true);

					if ((bmpMetadata.Copyright != null) && (!String.IsNullOrEmpty(bmpMetadata.Copyright.Trim())))
						metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.Copyright, Resources.Metadata_Copyright, bmpMetadata.Copyright.Trim(), true);

					if ((bmpMetadata.Subject != null) && (!String.IsNullOrEmpty(bmpMetadata.Subject.Trim())))
						metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.Subject, Resources.Metadata_Subject, bmpMetadata.Subject.Trim(), true);
				}
				catch (NotSupportedException) { } // Some image types, such as png, throw a NotSupportedException. Let's swallow them and move on.
			}
		}

		/// <summary>
		/// Adds GPS data from the <paramref name="bmpMetadata" /> to the <paramref name="metadataItems" /> collection.
		/// </summary>
		/// <param name="bmpMetadata">An object containing the metadata.</param>
		/// <param name="metadataItems">The metadata items.</param>
		private static void AddGpsMetadata(BitmapMetadata bmpMetadata, IGalleryObjectMetadataItemCollection metadataItems)
		{
			GpsLocation gps = GpsLocation.Parse(bmpMetadata);

			if (!String.IsNullOrEmpty(gps.Version))
			{
				metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.GpsVersion, GetResource(FormattedMetadataItemName.GpsVersion), gps.Version, true);
			}

			if ((gps.Latitude != null) && (gps.Longitude != null))
			{
				metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.GpsLocation, GetResource(FormattedMetadataItemName.GpsLocation), gps.ToLatitudeLongitudeDecimalString(), true);
				metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.GpsLatitude, GetResource(FormattedMetadataItemName.GpsLatitude), gps.Latitude.ToDouble().ToString("F6", CultureInfo.InvariantCulture), true);
				metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.GpsLongitude, GetResource(FormattedMetadataItemName.GpsLongitude), gps.Longitude.ToDouble().ToString("F6", CultureInfo.InvariantCulture), true);
			}

			if (gps.Altitude.HasValue)
			{
				string altitude = String.Concat(gps.Altitude.Value.ToString("N0"), " ", Resources.Metadata_meters);
				metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.GpsAltitude, GetResource(FormattedMetadataItemName.GpsAltitude), altitude, true);
			}

			if ((gps.DestLatitude != null) && (gps.DestLongitude != null))
			{
				metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.GpsDestLocation, GetResource(FormattedMetadataItemName.GpsDestLocation), gps.ToDestLatitudeLongitudeDecimalString(), true);
				metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.GpsDestLatitude, GetResource(FormattedMetadataItemName.GpsDestLatitude), gps.DestLatitude.ToDouble().ToString("F6", CultureInfo.InvariantCulture), true);
				metadataItems.AddNew(int.MinValue, FormattedMetadataItemName.GpsDestLongitude, GetResource(FormattedMetadataItemName.GpsDestLongitude), gps.DestLongitude.ToDouble().ToString("F6", CultureInfo.InvariantCulture), true);
			}
		}

		private static void AddIptcMetadata(BitmapMetadata bmpMetadata, IGalleryObjectMetadataItemCollection metadataItems)
		{
			foreach (KeyValuePair<FormattedMetadataItemName, string> iptcQueryParm in GetIptcQueryParameters())
			{
				string iptcValue = bmpMetadata.GetQuery(String.Format(IptcQueryFormatString, iptcQueryParm.Value)) as string;

				if (!String.IsNullOrEmpty(iptcValue))
				{
					switch (iptcQueryParm.Key)
					{
						case FormattedMetadataItemName.IptcDateCreated:
							{
								DateTime dateTaken = TryParseDate(iptcValue);

								if (dateTaken.Year > DateTime.MinValue.Year)
								{
									metadataItems.AddNew(int.MinValue, iptcQueryParm.Key, GetResource(iptcQueryParm.Key), dateTaken.ToString(DateTimeFormatStringForMetadata, CultureInfo.InvariantCulture), true);
								}
								break;
							}
						default:
							{
								metadataItems.AddNew(int.MinValue, iptcQueryParm.Key, GetResource(iptcQueryParm.Key), iptcValue, true);
								break;
							}
					}
				}
			}
		}

		/// <summary>
		/// Try to convert <paramref name="dteRaw" /> to a valid <see cref="DateTime" /> object. If it cannot be converted, return
		/// <see cref="DateTime.MinValue" />.
		/// </summary>
		/// <param name="dteRaw">The string containing the date/time to convert.</param>
		/// <returns>Returns a <see cref="DateTime" /> instance.</returns>
		/// <remarks>The IPTC specs do not define an exact format for the ITPC Date Created field, so it is unclear how to reliably parse
		/// it. However, an analysis of sample photos, including those provided by IPTC (http://www.iptc.org), show that the format
		/// yyyyMMdd is consistently used, so we'll try that if the more generic parsing doesnt work.</remarks>
		private static DateTime TryParseDate(string dteRaw)
		{
			DateTime result;
			if (DateTime.TryParse(dteRaw, out result))
			{
				return result;
			}
			else if (DateTime.TryParseExact(dteRaw, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
			{
				return result;
			}

			return DateTime.MinValue;
		}

		private static string GetResource(FormattedMetadataItemName formattedMetadataItemName)
		{
			const string resourcePrefix = "Metadata_";

			return Resources.ResourceManager.GetString(String.Concat(resourcePrefix, formattedMetadataItemName.ToString())) ?? formattedMetadataItemName.ToString();
		}

		private static Dictionary<FormattedMetadataItemName, string> GetIptcQueryParameters()
		{
			Dictionary<FormattedMetadataItemName, string> iptcQueryParms = new Dictionary<FormattedMetadataItemName, string>();

			iptcQueryParms.Add(FormattedMetadataItemName.IptcByline, "By-Line");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcBylineTitle, "By-line Title");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcCaption, "Caption");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcCity, "City");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcCopyrightNotice, "Copyright Notice");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcCountryPrimaryLocationName, "Country/Primary Location Name");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcCredit, "Credit");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcDateCreated, "Date Created");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcHeadline, "Headline");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcKeywords, "Keywords");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcObjectName, "Object Name");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcOriginalTransmissionReference, "Original Transmission Reference");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcProvinceState, "Province/State");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcRecordVersion, "Record Version");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcSource, "Source");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcSpecialInstructions, "Special Instructions");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcSublocation, "Sub-location");
			iptcQueryParms.Add(FormattedMetadataItemName.IptcWriterEditor, "Writer/Editor");

			return iptcQueryParms;
		}

		/// <summary>
		/// Get a reference to the BitmapMetadata object for this image file that contains the metadata such as title, keywords, etc.
		/// Returns null if the metadata is not accessible.
		/// </summary>
		/// <returns> Returns a reference to the BitmapMetadata object for this image file that contains the metadata such as title, keywords, etc.</returns>
		/// <remarks>A BitmapDecoder object is created from the absolute filepath passed into the constructor. Through trial and
		/// error, the relevant metadata appears to be stored in the first frame in the BitmapDecoder property of the first frame
		/// of the root-level BitmapDecoder object. One might expect the Metadata property of the root-level BitmapDecoder object to
		/// contain the metadata, but it seems to always be null.</remarks>
		private static BitmapMetadata GetBitmapMetadata(string imageFilePath)
		{
			// Do not use the BitmapCacheOption.Default or None option, as it will hold a lock on the file until garbage collection. I discovered
			// this problem and it has been submitted to MS as a bug. See thread in the managed newsgroup:
			// http://www.microsoft.com/communities/newsgroups/en-us/default.aspx?dg=microsoft.public.dotnet.framework&tid=b694ada2-10c4-4999-81f8-97295eb024a9&cat=en_US_a4ab6128-1a11-4169-8005-1d640f3bd725&lang=en&cr=US&sloc=en-us&m=1&p=1
			// Also do not use BitmapCacheOption.OnLoad as suggested in the thread, as it causes the memory to not be released until 
			// eventually IIS crashes when you do things like synchronize 100 images.
			// BitmapCacheOption.OnDemand seems to be the only option that doesn't lock the file or crash IIS.
			// Update 2007-07-29: OnDemand seems to also lock the file. There is no good solution! Acckkk
			// Update 2007-08-04: After installing VS 2008 beta 2, which also installs .NET 2.0 SP1, I discovered that OnLoad no longer crashes IIS.
			// Update 2008-05-19: The Create method doesn't release the file lock when an exception occurs, such as when the file is a WMF. See:
			// http://www.microsoft.com/communities/newsgroups/en-us/default.aspx?dg=microsoft.public.dotnet.framework&tid=fe3fb82f-0191-40a3-b789-0602cc4445d3&cat=&lang=&cr=&sloc=&p=1
			// Bug submission: https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=344914
			// The workaround is to use a different overload of Create that takes a FileStream.

			if (String.IsNullOrEmpty(imageFilePath))
				return null;

			BitmapDecoder fileBitmapDecoder;
			using (Stream stream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
			{
				try
				{
					fileBitmapDecoder = BitmapDecoder.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
					// DO NOT USE: fileBitmapDecoder = BitmapDecoder.Create(new Uri(imageFilePath, UriKind.Absolute), BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
				}
				catch (NotSupportedException)
				{
					// Some image types, such as wmf, throw an exception. Let's skip trying to get the metadata.
					return null;
				}
			}

			if ((fileBitmapDecoder == null) || (fileBitmapDecoder.Frames.Count == 0))
				return null;

			BitmapFrame fileFirstFrame = fileBitmapDecoder.Frames[0];

			if (fileFirstFrame == null)
				return null;

			BitmapDecoder firstFrameBitmapDecoder = fileFirstFrame.Decoder;

			if ((firstFrameBitmapDecoder == null) || (firstFrameBitmapDecoder.Frames.Count == 0))
				return null;

			BitmapFrame firstFrameInDecoderInFirstFrameOfFile = firstFrameBitmapDecoder.Frames[0];

			// The Metadata property is of type ImageMetadata, so we must cast it to BitmapMetadata.
			return firstFrameInDecoderInFirstFrameOfFile.Metadata as BitmapMetadata;
		}

		private static string ConvertStringCollectionToDelimitedString(System.Collections.ObjectModel.ReadOnlyCollection<string> stringCollection)
		{
			const string delimiter = "; ";
			string[] strings = new string[stringCollection.Count];
			stringCollection.CopyTo(strings, 0);
			return String.Join(delimiter, strings);
		}

	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Software Developer (Senior) Tech Info Systems
United States United States
I have nearly 20 years of industry experience in software development, architecture, and Microsoft Office products. My company Tech Info Systems provides custom software development services for corporations, governments, and other organizations. Tech Info Systems is a registered member of the Microsoft Partner Program and I am a Microsoft Certified Professional Developer (MCPD).

I am the creator and lead developer of Gallery Server Pro, a free, open source ASP.NET gallery for sharing photos, video, audio, documents, and other files over the web. It has been developed over several years and has involved thousands of hours. The end result is a robust, configurable, and professional grade gallery that can be integrated into your web site, whether you are a large corporation, small business, professional photographer, or a local church.

Comments and Discussions