Click here to Skip to main content
15,886,110 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.Drawing;
using System.Globalization;
using System.IO;
using System.Drawing.Imaging;
using GalleryServerPro.Business.Interfaces;

namespace GalleryServerPro.Business
{
	/// <summary>
	/// The Watermark class contains functionality for applying a text and/or image watermark to an image.
	/// </summary>
	public class Watermark : IDisposable
	{
		#region Private Fields

		private ImageAttributes _imageAttributes;

		private System.Drawing.Image _watermarkImage;
		private int _watermarkImageWidth = int.MinValue;
		private int _watermarkImageHeight = int.MinValue;
		private string _imagePath;
		private ContentAlignment _imageLocation;
		private int _imageWidthPercent;
		private int _imageOpacityPercent;

		private string _watermarkText;
		private System.Drawing.Color _textColor;
		private int _textHeightPixels;
		private int _textWidthPercent;
		private string _textFontName;
		private ContentAlignment _textLocation;
		private int _textOpacityPercent;

		private const int MIN_FONT_HEIGHT_PIXELS = 4;
		private const int DEFAULT_TEXT_HEIGHT_PIXELS = 12; // Text height used if not specified.
		private const float BORDER_PERCENT = .01f; // The distance from the border to place the watermark text and image. Ex: .01
		// means border thickness should be 1% of the width of the recipient image.
		private bool _hasBeenDisposed; // Used by Dispose() methods

		#endregion

		#region Public Properties

		/// <summary>
		/// Gets or sets the location for the watermark image on the recipient image.
		/// </summary>
		/// <value>The image location.</value>
		public ContentAlignment ImageLocation
		{
			get { return _imageLocation; }
			set { _imageLocation = value; }
		}

		/// <summary>
		/// Gets or sets the percent of the overall width of the recipient image that should be covered with the
		/// watermark image. The size of the image is automatically scaled to achieve the desired width. For example,
		/// a value of 50 means the watermark image is 50% as wide as the recipient image. Valid values are 0 - 100.
		/// A value of 0 turns off this feature and causes the image to be rendered its actual size.
		/// </summary>
		/// <value>The image width, in percent.</value>
		public int ImageWidthPercent
		{
			get { return _imageWidthPercent; }
			set { _imageWidthPercent = value; }
		}

		/// <summary>
		/// Gets or sets the watermark text to be applied to the recipient image.
		/// </summary>
		/// <value>The watermark text.</value>
		public string WatermarkText
		{
			get { return _watermarkText; }
			set { _watermarkText = value; }
		}

		/// <summary>
		/// Gets or sets the height, in pixels, of the watermark text. This value is ignored if the property
		/// TextWidthPercent is non-zero. Valid values are 0 - 10000.
		/// </summary>
		/// <value>The text height, in pixels.</value>
		public int TextHeightPixels
		{
			get { return _textHeightPixels; }
			set
			{
				if ((value < 0) || (value > 10000))
				{
					throw new ArgumentOutOfRangeException(String.Format(CultureInfo.CurrentCulture, "TextHeightPixels must be an integer between 0 and 10000. The value {0} is invalid.", value));
				}
				_textHeightPixels = value;
			}
		}

		/// <summary>
		/// Gets or sets the percent of the overall width of the recipient image that should be covered with the
		/// watermark text. The size of the text is automatically scaled up or down to achieve the desired width. For example,
		/// a value of 50 means the text is 50% as wide as the recipient image. Valid values are 0 - 100. The text is never
		/// rendered in a font smaller than 6 pixels, so in cases of long text it may stretch wider than the percentage
		/// specified in this setting. A value of 0 turns off this feature and causes the text size to be determined by the
		/// TextSizePixels property.
		/// </summary>
		/// <value>The text width, in percent.</value>
		public int TextWidthPercent
		{
			get { return _textWidthPercent; }
			set
			{
				if ((value < 0) || (value > 100))
				{
					throw new ArgumentOutOfRangeException(String.Format(CultureInfo.CurrentCulture, "TextWidthPercent must be an integer between 0 and 100. The value {0} is invalid.", value));
				}

				_textWidthPercent = value;
			}
		}

		/// <summary>
		/// Gets or sets the font family name to use for the watermark text applied to the recipient image.
		/// If the name does not represent a font installed on the server, a generic sans serif font is used.
		/// </summary>
		/// <value>The name of the text font.</value>
		public string TextFontName
		{
			get { return _textFontName; }
			set { _textFontName = value; }
		}

		/// <summary>
		/// Gets or sets the location for the watermark text on the recipient image.
		/// </summary>
		/// <value>The text location.</value>
		public ContentAlignment TextLocation
		{
			get { return _textLocation; }
			set { _textLocation = value; }
		}

		/// <summary>
		/// Gets or sets the color of the watermark text.
		/// </summary>
		/// <value>The color of the text.</value>
		public System.Drawing.Color TextColor
		{
			get { return _textColor; }
			set { _textColor = value; }
		}

		/// <summary>
		/// Gets or sets the opacity of the watermark text. Valid values are 0 - 100, with 0 being completely
		/// transparent and 100 completely opaque.
		/// </summary>
		/// <value>The text opacity, in percent.</value>
		public int TextOpacityPercent
		{
			get { return _textOpacityPercent; }
			set { _textOpacityPercent = value; }
		}

		/// <summary>
		/// Gets or sets the opacity of the watermark image. Valid values are 0 - 100, with 0 being completely
		/// transparent and 100 completely opaque.
		/// </summary>
		/// <value>The image opacity, in percent.</value>
		public int ImageOpacityPercent
		{
			get { return _imageOpacityPercent; }
			set { _imageOpacityPercent = value; }
		}

		/// <summary>
		/// Gets or sets the full path to a watermark image to be applied to the recipient image. The image
		/// must be in a format that allows it to be instantiated in a System.Drawing.Bitmap object. If a relative
		/// path is assigned to this property, it is combined with the current application's path and checked to ensure
		/// it exists. A System.IO.FileNotFoundException is thrown if this property is assigned a non-empty value and
		/// the value does not represent a file on the hard drive. Setting this property
		/// also assigns the WatermarkImage property. An exception is thrown if .NET is unable to create a
		/// System.Drawing.Image object from the file path. Returns String.Empty if user did not specify a value in
		/// the configuration file.
		/// </summary>
		/// <value>The full path to a watermark image to be applied to the recipient image.</value>
		public string ImagePath
		{
			get { return _imagePath; }
			set
			{
				if (String.IsNullOrEmpty(value))
				{
					_imagePath = String.Empty;
					_watermarkImage = null;
					return;
				}
				else if (File.Exists(value))
				{
					_imagePath = value; // File exists. OK to set property.
				}
				else
				{
					// File doesn't exist, but maybe user specified a relative path. Combine with the application path and try again.
					string relativePath = value.TrimStart(new char[] { '/', '\\' }).Replace("/", "\\");
					string fullPath = Path.Combine(AppSetting.Instance.PhysicalApplicationPath, relativePath);

					if (!File.Exists(fullPath))
					{
						throw new System.IO.FileNotFoundException(String.Format(CultureInfo.CurrentCulture, "No image file exists at {0} or {1}. Check the configuration file to ensure the watermarkImagePath setting points to a valid file that exists in the specified location.", value, fullPath));
					}

					// File exists! Assign.
					_imagePath = fullPath;
				}

				// Assign the WatermarkImage property. We clone it and dispose of the first instance to release the lock on the file.
				// If we don't do this, the watermark image is locked for the lifetime of the application.
				_watermarkImage = System.Drawing.Image.FromFile(_imagePath);
				System.Drawing.Image watermarkImageCopy = (System.Drawing.Image)_watermarkImage.Clone();
				_watermarkImage.Dispose();
				_watermarkImage = watermarkImageCopy;
			}
		}

		/// <summary>
		/// Gets the watermark image to be applied to the recipient image. The image is created when the
		/// ImagePath property is assigned. Returns null if ImagePath is not specified (that is, the user did
		/// not enter a value in the watermarkImagePath property in the configuration file).
		/// </summary>
		/// <value>The watermark image to be applied to the recipient image.</value>
		public System.Drawing.Image WatermarkImage
		{
			get { return _watermarkImage; }
		}

		/// <summary>
		/// Gets the width, in pixels, of the watermark image. Returns int.MinValue if no watermark image is specified.
		/// </summary>
		/// <value>The width, in pixels, of the watermark image.</value>
		public int WatermarkImageWidth
		{
			get
			{
				if ((_watermarkImageWidth == int.MinValue) && (this.WatermarkImage != null))
				{
					_watermarkImageWidth = this.WatermarkImage.Width;
				}

				return _watermarkImageWidth;
			}
		}

		/// <summary>
		/// Gets the height, in pixels, of the watermark image. Returns int.MinValue if no watermark image is specified.
		/// </summary>
		/// <value>The height, in pixels, of the watermark image.</value>
		public int WatermarkImageHeight
		{
			get
			{
				if ((_watermarkImageHeight == int.MinValue) && (this.WatermarkImage != null))
				{
					_watermarkImageHeight = this.WatermarkImage.Height;
				}

				return _watermarkImageHeight;
			}
		}

		#endregion

		#region Constructors

		private Watermark()
		{
		}

		#endregion

		#region Destructor (finalizer)

		/// <summary>
		/// Releases unmanaged resources and performs other cleanup operations before the
		/// <see cref="Watermark"/> is reclaimed by garbage collection.
		/// </summary>
		~Watermark()
		{
			Dispose(false);
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Overlay the text and/or image watermark over the image specified in the <paramref name="filePath"/> parameter and return.
		/// </summary>
		/// <param name="filePath">A string representing the full path to the image file  
		/// (e.g. "C:\mypics\myprettypony.jpg", "myprettypony.jpg").</param>
		/// <returns>Returns a <see cref="System.Drawing.Image" /> instance containing the image with the watermark applied.</returns>
		public System.Drawing.Image ApplyWatermark(string filePath)
		{
			System.Drawing.Image img = System.Drawing.Image.FromFile(filePath);

			ApplyTextWatermark(img);

			if (this.WatermarkImage != null)
			{
				System.Drawing.Image watermarkedImage = ApplyImageWatermark(img);
				img.Dispose();
				return watermarkedImage;
			}
			else
			{
				return img;
			}
		}

		/// <summary>
		/// Gets the watermark that is configured for the specified <paramref name="galleryId" />.
		/// </summary>
		/// <param name="galleryId">The gallery ID.</param>
		/// <returns>Returns a <see cref="Watermark" /> instance.</returns>
		public static Watermark GetUserSpecifiedWatermark(int galleryId)
		{
			IGallerySettings gallerySetting = Factory.LoadGallerySetting(galleryId);

			Watermark tempWatermark = new Watermark();
			tempWatermark.WatermarkText = gallerySetting.WatermarkText;
			tempWatermark.TextFontName = gallerySetting.WatermarkTextFontName;
			tempWatermark.TextColor = HelperFunctions.GetColor(gallerySetting.WatermarkTextColor);
			tempWatermark.TextHeightPixels = gallerySetting.WatermarkTextFontSize;
			tempWatermark.TextWidthPercent = gallerySetting.WatermarkTextWidthPercent;
			tempWatermark.TextOpacityPercent = gallerySetting.WatermarkTextOpacityPercent;
			tempWatermark.TextLocation = gallerySetting.WatermarkTextLocation;
			tempWatermark.ImagePath = gallerySetting.WatermarkImagePath;
			tempWatermark.ImageWidthPercent = gallerySetting.WatermarkImageWidthPercent;
			tempWatermark.ImageOpacityPercent = gallerySetting.WatermarkImageOpacityPercent;
			tempWatermark.ImageLocation = gallerySetting.WatermarkImageLocation;

			return tempWatermark;
		}

		/// <summary>
		/// Gets the watermark to use when the application is in reduced functionality mode.
		/// </summary>
		/// <param name="galleryId">The gallery ID.</param>
		/// <returns>Returns a <see cref="Watermark" /> instance.</returns>
		public static Watermark GetReducedFunctionalityModeWatermark(int galleryId)
		{
			IGallerySettings gallerySetting = Factory.LoadGallerySetting(galleryId);

			Watermark tempWatermark = new Watermark();
			tempWatermark.WatermarkText = GalleryServerPro.Business.Properties.Resources.Reduced_Functionality_Mode_Watermark_Text;
			tempWatermark.TextFontName = gallerySetting.WatermarkTextFontName;
			tempWatermark.TextColor = HelperFunctions.GetColor(gallerySetting.WatermarkTextColor);
			tempWatermark.TextHeightPixels = 0;
			tempWatermark.TextWidthPercent = 100;
			tempWatermark.TextOpacityPercent = 100;
			tempWatermark.TextLocation = ContentAlignment.MiddleCenter;
			tempWatermark._watermarkImage = GalleryServerPro.Business.Properties.Resources.GSP_Logo;
			tempWatermark.ImageWidthPercent = 85;
			tempWatermark.ImageOpacityPercent = 50;
			tempWatermark.ImageLocation = ContentAlignment.BottomCenter;

			return tempWatermark;
		}

		#endregion

		#region Private Methods

		private System.Drawing.Image ApplyImageWatermark(System.Drawing.Image recipientImage)
		{
			if (recipientImage == null)
				throw new ArgumentNullException("recipientImage");

			if (this.WatermarkImage == null)
				return recipientImage;

			// Create a Bitmap from the image we are going to draw the watermark on.
			Bitmap recipientBitmap = new Bitmap(recipientImage);
			recipientBitmap.SetResolution(recipientImage.HorizontalResolution, recipientImage.VerticalResolution);

			int recipientImageWidth = recipientImage.Width;
			int recipientImageHeight = recipientImage.Height;

			// Get the watermark image, scaling it up or down if needed.
			System.Drawing.Image watermarkImage = GetWatermarkImage(recipientImageWidth, recipientImageHeight);

			int watermarkWidth = watermarkImage.Width;
			int watermarkHeight = watermarkImage.Height;

			// Turn off the border if the watermark image is too big to allow for it.
			float borderPercent = BORDER_PERCENT;
			if ((watermarkHeight > (recipientImageHeight - (recipientImageHeight * borderPercent))) ||
					(watermarkWidth > (recipientImageWidth - (recipientImageWidth * borderPercent))))
			{
				borderPercent = 0;
			}

			// Get the X and Y position for where to start drawing the watermark image on the recipient image.
			Point watermarkStartingPoint = GetWatermarkStartingPoint((float)watermarkWidth, (float)watermarkHeight, (float)recipientImageWidth, (float)recipientImageHeight, this.ImageLocation, borderPercent);

			// Draw the watermark image on the recipient image.
			using (Graphics grWatermark = Graphics.FromImage(recipientBitmap))
			{
				grWatermark.DrawImage(watermarkImage,
															new Rectangle(watermarkStartingPoint.X, watermarkStartingPoint.Y, watermarkWidth, watermarkHeight),  //Set the destination position
															0,                  // x-coordinate of recipient image to start drawing watermark 
															0,                  // y-coordinate of of recipient image to start drawing watermark
															watermarkWidth,
															watermarkHeight,
															GraphicsUnit.Pixel,
															GetWatermarkImageAttributes(this.ImageOpacityPercent));

				//Replace the original image with the one with the newly drawn watermark image.
				recipientImage = recipientBitmap;
			}

			return recipientImage;
		}

		private System.Drawing.Image GetWatermarkImage(int recipientImageWidth, int recipientImageHeight)
		{
			int watermarkWidth = this.WatermarkImageWidth;
			int watermarkHeight = this.WatermarkImageHeight;

			if (this.ImageWidthPercent > 0)
			{
				// We need to resize the watermark image so that its width takes up the specified percentage of
				// the overall width of the recipient image.
				int resizedWatermarkWidth = (int)(recipientImageWidth * (((float)this.ImageWidthPercent) / 100));
				int resizedWatermarkHeight = (resizedWatermarkWidth * watermarkHeight) / watermarkWidth;

				// If the resized height is taller than the recipient image, then readjust the width and height
				// to make the watermark as tall as the recipient image.
				if (resizedWatermarkHeight > recipientImageHeight)
				{
					resizedWatermarkHeight = recipientImageHeight;
					resizedWatermarkWidth = (watermarkWidth * resizedWatermarkHeight) / watermarkHeight;
				}

				// Get the resized image and assign the width and height vars.
				return ImageHelper.CreateResizedBitmap(this.WatermarkImage, watermarkWidth, watermarkHeight, resizedWatermarkWidth, resizedWatermarkHeight);
			}
			else
			{
				return this.WatermarkImage;
			}
		}

		private ImageAttributes GetWatermarkImageAttributes(int imageOpacityPercent)
		{
			// Change the opacity of the watermark.  This is done by applying a 5x5 matrix that contains the 
			// coordinates for the RGBA space.  Set the 3rd row and 3rd column to the desired opacity. (0.0 - 1.0).

			if (_imageAttributes != null)
				return _imageAttributes;

			float opacity = imageOpacityPercent / 100.0f;
			float[][] colorMatrixElements = { 
				new float[] {1.0f,  0.0f,  0.0f,  0.0f, 0.0f},       
				new float[] {0.0f,  1.0f,  0.0f,  0.0f, 0.0f},        
				new float[] {0.0f,  0.0f,  1.0f,  0.0f, 0.0f},        
				new float[] {0.0f,  0.0f,  0.0f,  opacity, 0.0f},        
				new float[] {0.0f,  0.0f,  0.0f,  0.0f, 1.0f}};
			ColorMatrix wmColorMatrix = new ColorMatrix(colorMatrixElements);

			_imageAttributes = new ImageAttributes();
			_imageAttributes.SetColorMatrix(wmColorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Default);
			_imageAttributes.SetColorKey(Color.Transparent, Color.Transparent);

			return _imageAttributes;
		}

		private void ApplyTextWatermark(System.Drawing.Image img)
		{
			if (String.IsNullOrEmpty(this.WatermarkText))
				return;

			int opacity = (int)((255.0f * this._textOpacityPercent) / 100.0f);
			int recipientImageWidth = img.Width;
			int recipientImageHeight = img.Height;

			Font font = null;
			Graphics gr = null;
			try
			{
				gr = Graphics.FromImage(img);

				#region Generate font

				if (TextWidthPercent == 0)
				{
					// We want to use the TextHeightPixels property to set the size.
					int fontSize = (this.TextHeightPixels == 0 ? DEFAULT_TEXT_HEIGHT_PIXELS : this.TextHeightPixels);
					font = new Font(this.TextFontName, fontSize);
				}
				else
				{
					// We have a value for TextWidthPercent, which means we want to create a font/size combination
					// whose width takes up the specified percentage across the recipient image.
					int fontSize = MIN_FONT_HEIGHT_PIXELS;
					float maxTextWidth = recipientImageWidth * (TextWidthPercent / 100.0f);
					font = new Font(this.TextFontName, fontSize);

					// Starting with the default minimum font size, keep increasing it until we reach the desired width. Note that
					// we may end up with a font height taller than the image. An early version of this routine limited the font height 
					// to no larger than the recipient image height, but that resulted in undesirable empty space above and below the 
					// text, since the measured height includes space for all characters in the character set. This created the 
					// impression that the character was not really as tall as the recipient image. 'tis better to let the watermark text be
					// taller than the image in certain circumstances - the user can always reduce the TextWidthPercent until the 
					// desired height is achieved.
					while (gr.MeasureString(this.WatermarkText, font).Width < maxTextWidth)
					{
						fontSize += 1;
						font.Dispose();
						font = new Font(this.TextFontName, fontSize);
					}

					// At this point the font size is one larger than it should be. Reduce it and create the final font object.
					fontSize -= 1;
					font.Dispose();
					font = new Font(this.TextFontName, fontSize);
				}

				#endregion

				SizeF watermarkSize = gr.MeasureString(this.WatermarkText, font);

				// Turn off the border if the watermark text is too big to allow for it.
				float borderPercent = BORDER_PERCENT;
				if ((watermarkSize.Height > (recipientImageHeight - (recipientImageHeight * borderPercent))) ||
						(watermarkSize.Width > (recipientImageWidth - (recipientImageWidth * borderPercent))))
				{
					borderPercent = 0;
				}

				Point textStartingPoint = GetWatermarkStartingPoint(watermarkSize.Width, watermarkSize.Height, (float)recipientImageWidth, (float)recipientImageHeight, this.TextLocation, borderPercent);

				using (Brush semitransparentBrush = new SolidBrush(Color.FromArgb(opacity, this.TextColor)))
				{
					gr.DrawString(this.WatermarkText, font, semitransparentBrush, textStartingPoint);
				}
			}
			finally
			{
				if (font != null)
				{
					font.Dispose();
				}
				if (gr != null)
				{
					gr.Dispose();
				}
			}
		}

		private static Point GetWatermarkStartingPoint(float watermarkWidth, float watermarkHeight, float imageWidth, float imageHeight, ContentAlignment watermarkLocation, float borderPercent)
		{
			Point startingPoint = new Point();
			switch (watermarkLocation)
			{
				case ContentAlignment.TopLeft:
					startingPoint.X = (int)(imageWidth * borderPercent);
					startingPoint.Y = (int)(imageHeight * borderPercent);
					break;
				case ContentAlignment.TopCenter:
					startingPoint.X = (int)(imageWidth - watermarkWidth) / 2;
					startingPoint.Y = (int)(imageHeight * borderPercent);
					break;
				case ContentAlignment.TopRight:
					startingPoint.X = (int)(imageWidth - watermarkWidth - (imageWidth * borderPercent));
					startingPoint.Y = (int)(imageHeight * borderPercent);
					break;
				case ContentAlignment.MiddleLeft:
					startingPoint.X = (int)(imageWidth * borderPercent);
					startingPoint.Y = (int)(imageHeight - watermarkHeight) / 2;
					break;
				case ContentAlignment.MiddleCenter:
					startingPoint.X = (int)(imageWidth - watermarkWidth) / 2;
					startingPoint.Y = (int)(imageHeight - watermarkHeight) / 2;
					break;
				case ContentAlignment.MiddleRight:
					startingPoint.X = (int)(imageWidth - watermarkWidth - (imageWidth * borderPercent));
					startingPoint.Y = (int)(imageHeight - watermarkHeight) / 2;
					break;
				case ContentAlignment.BottomLeft:
					startingPoint.X = (int)(imageWidth * borderPercent);
					startingPoint.Y = (int)(imageHeight - watermarkHeight - (imageHeight * borderPercent));
					break;
				case ContentAlignment.BottomCenter:
					startingPoint.X = (int)(imageWidth - watermarkWidth) / 2;
					startingPoint.Y = (int)(imageHeight - watermarkHeight - (imageHeight * borderPercent));
					break;
				case ContentAlignment.BottomRight:
					startingPoint.X = (int)(imageWidth - watermarkWidth - (imageWidth * borderPercent));
					startingPoint.Y = (int)(imageHeight - watermarkHeight - (imageHeight * borderPercent));
					break;
				default:
					startingPoint.X = (int)(imageWidth * borderPercent);
					startingPoint.Y = (int)(imageHeight * borderPercent);
					break;
			}

			return startingPoint;
		}

		#endregion

		#region IDisposable

		/// <summary>
		/// Releases unmanaged and - optionally - managed resources
		/// </summary>
		/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
		protected virtual void Dispose(bool disposing)
		{
			if (!this._hasBeenDisposed)
			{
				// Dispose of resources held by this instance.
				if (this._watermarkImage != null)
				{
					this._watermarkImage.Dispose();
				}

				// Set the sentinel.
				this._hasBeenDisposed = true;
			}
		}

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		#endregion
	}
}

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