// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // // Encapsulates methods for processing image files. // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor { #region Using using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Threading; using ImageProcessor.Extensions; using ImageProcessor.Imaging; using ImageProcessor.Processors; #endregion /// /// Encapsulates methods for processing image files. /// public class ImageFactory : IDisposable { #region Fields /// /// The default quality for jpeg files. /// private const int DefaultJpegQuality = 90; /// /// The backup image format. /// private ImageFormat backupImageFormat; /// /// The memory stream for storing any input stream to prevent disposal. /// private MemoryStream inputStream; /// /// Whether the image is indexed. /// private bool isIndexed; /// /// A value indicating whether this instance of the given entity has been disposed. /// /// if this instance has been disposed; otherwise, . /// /// If the entity is disposed, it must not be disposed a second /// time. The isDisposed field is set the first time the entity /// is disposed. If the isDisposed field is true, then the Dispose() /// method will not dispose again. This help not to prolong the entity's /// life in the Garbage Collector. /// private bool isDisposed; #endregion #region Destructors /// /// Finalizes an instance of the ImageFactory class. /// /// /// Use C# destructor syntax for finalization code. /// This destructor will run only if the Dispose method /// does not get called. /// It gives your base class the opportunity to finalize. /// Do not provide destructors in types derived from this class. /// ~ImageFactory() { // Do not re-create Dispose clean-up code here. // Calling Dispose(false) is optimal in terms of // readability and maintainability. this.Dispose(false); } #endregion #region Properties /// /// Gets the local image for manipulation. /// public Image Image { get; private set; } /// /// Gets the path to the local image for manipulation. /// public string ImagePath { get; private set; } /// /// Gets the query-string parameters for web image manipulation. /// public string QueryString { get; private set; } /// /// Gets a value indicating whether the image factory should process the file. /// public bool ShouldProcess { get; private set; } /// /// Gets the file format of the image. /// public ImageFormat ImageFormat { get; private set; } /// /// Gets the mime type. /// public string MimeType { get { return this.ImageFormat.GetMimeType(); } } /// /// Gets or sets the original extension. /// internal string OriginalExtension { get; set; } /// /// Gets or sets the quality of output for jpeg images as a percentile. /// internal int JpegQuality { get; set; } #endregion #region Methods /// /// Loads the image to process. Always call this method first. /// /// /// The containing the image information. /// /// /// The current instance of the class. /// public ImageFactory Load(MemoryStream memoryStream) { // Set our image as the memory stream value. this.Image = Image.FromStream(memoryStream, true); // Store the stream so we can dispose of it later. this.inputStream = memoryStream; // Set the other properties. this.JpegQuality = DefaultJpegQuality; this.ImageFormat = this.Image.RawFormat; this.backupImageFormat = this.ImageFormat; this.isIndexed = ImageUtils.IsIndexed(this.Image); this.ShouldProcess = true; return this; } /// /// Loads the image to process. Always call this method first. /// /// The absolute path to the image to load. /// /// The current instance of the class. /// public ImageFactory Load(string imagePath) { // Remove any querystring parameters passed by web requests. string[] paths = imagePath.Split('?'); string path = paths[0]; string query = string.Empty; if (paths.Length > 1) { query = paths[1]; } if (File.Exists(path)) { this.ImagePath = path; this.QueryString = query; // Open a file stream to prevent the need for lock. using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read)) { MemoryStream memoryStream = new MemoryStream(); // Copy the stream. fileStream.CopyTo(memoryStream); // Set the position to 0 afterwards. fileStream.Position = memoryStream.Position = 0; // Set our image as the memory stream value. this.Image = Image.FromStream(memoryStream, true); // Store the stream so we can dispose of it later. this.inputStream = memoryStream; // Set the other properties. this.JpegQuality = DefaultJpegQuality; ImageFormat imageFormat = this.Image.RawFormat; this.backupImageFormat = imageFormat; this.OriginalExtension = Path.GetExtension(this.ImagePath); this.ImageFormat = imageFormat; this.isIndexed = ImageUtils.IsIndexed(this.Image); this.ShouldProcess = true; } } return this; } /// /// Updates the specified image. Used by the various IProcessors. /// /// The image. /// /// The current instance of the class. /// public ImageFactory Update(Image image) { if (this.ShouldProcess) { this.Image = image; } return this; } /// /// Resets the current image to its original loaded state. /// /// /// The current instance of the class. /// public ImageFactory Reset() { if (this.ShouldProcess) { // Set our new image as the memory stream value. Image newImage = Image.FromStream(this.inputStream, true); // Dispose and reassign the image. this.Image.Dispose(); this.Image = newImage; // Set the other properties. this.JpegQuality = DefaultJpegQuality; this.ImageFormat = this.backupImageFormat; this.isIndexed = ImageUtils.IsIndexed(this.Image); } return this; } #region Manipulation /// /// Adds a query-string to the image factory to allow auto-processing of remote files. /// /// The query-string parameter to process. /// /// The current instance of the class. /// public ImageFactory AddQueryString(string query) { if (this.ShouldProcess) { this.QueryString = query; } return this; } /// /// Changes the opacity of the current image. /// /// /// The percentage by which to alter the images opacity. /// Any integer between 0 and 100. /// /// /// The current instance of the class. /// public ImageFactory Alpha(int percentage) { if (this.ShouldProcess) { // Sanitize the input. if (percentage > 100 || percentage < 0) { percentage = 0; } Alpha alpha = new Alpha { DynamicParameter = percentage }; this.Image = alpha.ProcessImage(this); } return this; } /// /// Changes the brightness of the current image. /// /// /// The percentage by which to alter the images brightness. /// Any integer between -100 and 100. /// /// /// The current instance of the class. /// public ImageFactory Brightness(int percentage) { if (this.ShouldProcess) { // Sanitize the input. if (percentage > 100 || percentage < -100) { percentage = 0; } Brightness brightness = new Brightness { DynamicParameter = percentage }; this.Image = brightness.ProcessImage(this); } return this; } /// /// Constrains the current image, resizing it to fit within the given dimensions whilst keeping its aspect ratio. /// /// /// The containing the maximum width and height to set the image to. /// /// /// The current instance of the class. /// public ImageFactory Constrain(Size size) { if (this.ShouldProcess) { ResizeLayer layer = new ResizeLayer(size, Color.Transparent, ResizeMode.Max); return this.Resize(layer); } return this; } /// /// Changes the contrast of the current image. /// /// /// The percentage by which to alter the images contrast. /// Any integer between -100 and 100. /// /// /// The current instance of the class. /// public ImageFactory Contrast(int percentage) { if (this.ShouldProcess) { // Sanitize the input. if (percentage > 100 || percentage < -100) { percentage = 0; } Contrast contrast = new Contrast { DynamicParameter = percentage }; this.Image = contrast.ProcessImage(this); } return this; } /// /// Crops the current image to the given location and size. /// /// /// The containing the coordinates to crop the image to. /// /// /// The current instance of the class. /// public ImageFactory Crop(Rectangle rectangle) { if (this.ShouldProcess) { Crop crop = new Crop { DynamicParameter = rectangle }; this.Image = crop.ProcessImage(this); } return this; } /// /// Applies a filter to the current image. /// /// /// The name of the filter to add to the image. /// /// /// The current instance of the class. /// public ImageFactory Filter(string filterName) { if (this.ShouldProcess) { Filter filter = new Filter { DynamicParameter = filterName }; this.Image = filter.ProcessImage(this); } return this; } /// /// Flips the current image either horizontally or vertically. /// /// /// Whether to flip the image vertically. /// /// /// The current instance of the class. /// public ImageFactory Flip(bool flipVertically) { if (this.ShouldProcess) { RotateFlipType rotateFlipType = flipVertically == false ? RotateFlipType.RotateNoneFlipX : RotateFlipType.RotateNoneFlipY; Flip flip = new Flip { DynamicParameter = rotateFlipType }; this.Image = flip.ProcessImage(this); } return this; } /// /// Sets the output format of the current image to the matching . /// /// The . to set the image to. /// Whether the pixel format of the image should be indexed. Used for generating Png8 images. /// /// The current instance of the class. /// [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")] public ImageFactory Format(ImageFormat imageFormat, bool indexedFormat = false) { if (this.ShouldProcess) { this.isIndexed = indexedFormat; this.ImageFormat = imageFormat; } return this; } /// /// Uses a Gaussian kernel to blur the current image. /// /// /// The sigma and threshold values applied to the kernel are /// 1.4 and 0 respectively. /// /// /// /// /// The size to set the Gaussian kernel to. /// /// /// The current instance of the class. /// public ImageFactory GaussianBlur(int size) { if (this.ShouldProcess && size > 0) { GaussianLayer layer = new GaussianLayer(size); GaussianBlur gaussianBlur = new GaussianBlur { DynamicParameter = layer }; this.Image = gaussianBlur.ProcessImage(this); } return this; } /// /// Uses a Gaussian kernel to blur the current image. /// /// /// The for applying sharpening and /// blurring methods to an image. /// /// /// The current instance of the class. /// public ImageFactory GaussianBlur(GaussianLayer gaussianLayer) { if (this.ShouldProcess) { GaussianBlur gaussianBlur = new GaussianBlur { DynamicParameter = gaussianLayer }; this.Image = gaussianBlur.ProcessImage(this); } return this; } /// /// Uses a Gaussian kernel to sharpen the current image. /// /// /// The sigma and threshold values applied to the kernel are /// 1.4 and 0 respectively. /// /// /// /// /// The size to set the Gaussian kernel to. /// /// /// The current instance of the class. /// public ImageFactory GaussianSharpen(int size) { if (this.ShouldProcess && size > 0) { GaussianLayer layer = new GaussianLayer(size); GaussianSharpen gaussianSharpen = new GaussianSharpen { DynamicParameter = layer }; this.Image = gaussianSharpen.ProcessImage(this); } return this; } /// /// Uses a Gaussian kernel to sharpen the current image. /// /// /// The for applying sharpening and /// blurring methods to an image. /// /// /// The current instance of the class. /// public ImageFactory GaussianSharpen(GaussianLayer gaussianLayer) { if (this.ShouldProcess) { GaussianSharpen gaussianSharpen = new GaussianSharpen { DynamicParameter = gaussianLayer }; this.Image = gaussianSharpen.ProcessImage(this); } return this; } /// /// Alters the output quality of the current image. /// /// This method will only effect the output quality of jpeg images /// /// /// A value between 1 and 100 to set the quality to. /// /// The current instance of the class. /// public ImageFactory Quality(int percentage) { if (this.ShouldProcess) { this.JpegQuality = percentage; } return this; } /// /// Resizes the current image to the given dimensions. /// /// /// The containing the width and height to set the image to. /// /// /// The current instance of the class. /// public ImageFactory Resize(Size size) { if (this.ShouldProcess) { int width = size.Width; int height = size.Height; var resizeSettings = new Dictionary { { "MaxWidth", width.ToString("G") }, { "MaxHeight", height.ToString("G") } }; ResizeLayer resizeLayer = new ResizeLayer(new Size(width, height)); Resize resize = new Resize { DynamicParameter = resizeLayer, Settings = resizeSettings }; this.Image = resize.ProcessImage(this); } return this; } /// /// Resizes the current image to the given dimensions. /// /// /// The containing the properties required to resize the image. /// /// /// The current instance of the class. /// public ImageFactory Resize(ResizeLayer resizeLayer) { if (this.ShouldProcess) { var resizeSettings = new Dictionary { { "MaxWidth", resizeLayer.Size.Width.ToString("G") }, { "MaxHeight", resizeLayer.Size.Height.ToString("G") } }; Resize resize = new Resize { DynamicParameter = resizeLayer, Settings = resizeSettings }; this.Image = resize.ProcessImage(this); } return this; } /// /// Rotates the current image by the given angle. /// /// /// The containing the properties to rotate the image. /// /// /// The current instance of the class. /// public ImageFactory Rotate(RotateLayer rotateLayer) { if (this.ShouldProcess) { // Sanitize the input. if (rotateLayer.Angle > 360 || rotateLayer.Angle < 0) { rotateLayer.Angle = 0; } Rotate rotate = new Rotate { DynamicParameter = rotateLayer }; this.Image = rotate.ProcessImage(this); } return this; } /// /// Adds rounded corners to the current image. /// /// /// The containing the properties to round corners on the image. /// /// /// The current instance of the class. /// public ImageFactory RoundedCorners(RoundedCornerLayer roundedCornerLayer) { if (this.ShouldProcess) { if (roundedCornerLayer.Radius < 0) { roundedCornerLayer.Radius = 0; } RoundedCorners roundedCorners = new RoundedCorners { DynamicParameter = roundedCornerLayer }; this.Image = roundedCorners.ProcessImage(this); } return this; } /// /// Changes the saturation of the current image. /// /// /// The percentage by which to alter the images saturation. /// Any integer between -100 and 100. /// /// /// The current instance of the class. /// public ImageFactory Saturation(int percentage) { if (this.ShouldProcess) { // Sanitize the input. if (percentage > 100 || percentage < -100) { percentage = 0; } Saturation saturate = new Saturation { DynamicParameter = percentage }; this.Image = saturate.ProcessImage(this); } return this; } /// /// Adds a vignette image effect to the current image. /// /// /// The current instance of the class. /// public ImageFactory Vignette() { if (this.ShouldProcess) { Vignette vignette = new Vignette(); this.Image = vignette.ProcessImage(this); } return this; } /// /// Adds a text based watermark to the current image. /// /// /// The containing the properties necessary to add /// the text based watermark to the image. /// /// /// The current instance of the class. /// public ImageFactory Watermark(TextLayer textLayer) { if (this.ShouldProcess) { Watermark watermark = new Watermark { DynamicParameter = textLayer }; this.Image = watermark.ProcessImage(this); } return this; } #endregion /// /// Saves the current image to the specified file path. /// /// The path to save the image to. /// /// The current instance of the class. /// public ImageFactory Save(string filePath) { if (this.ShouldProcess) { // We need to check here if the path has an extension and remove it if so. // This is so we can add the correct image format. int length = filePath.LastIndexOf(".", StringComparison.Ordinal); string extension = this.ImageFormat.GetFileExtension(this.OriginalExtension); if (!string.IsNullOrWhiteSpace(extension)) { filePath = length == -1 ? filePath + extension : filePath.Substring(0, length) + extension; } // Fix the colour palette of indexed images. this.FixIndexedPallete(); // ReSharper disable once AssignNullToNotNullAttribute DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(filePath)); if (this.ImageFormat.Equals(ImageFormat.Jpeg)) { // Jpegs can be saved with different settings to include a quality setting for the JPEG compression. // This improves output compression and quality. using (EncoderParameters encoderParameters = ImageUtils.GetEncodingParameters(this.JpegQuality)) { ImageCodecInfo imageCodecInfo = ImageCodecInfo.GetImageEncoders() .FirstOrDefault(ici => ici.MimeType.Equals(this.MimeType, StringComparison.OrdinalIgnoreCase)); if (imageCodecInfo != null) { for (int i = 0; i < 3; i++) { try { if (!directoryInfo.Exists) { directoryInfo.Create(); } this.Image.Save(filePath, imageCodecInfo, encoderParameters); break; } catch (Exception) { Thread.Sleep(200); } } } } } else { for (int i = 0; i < 3; i++) { try { if (!directoryInfo.Exists) { directoryInfo.Create(); } this.Image.Save(filePath, this.ImageFormat); break; } catch (Exception) { Thread.Sleep(200); } } } } return this; } /// /// Saves the current image to the specified output stream. /// /// /// The to save the image information to. /// /// /// The current instance of the class. /// public ImageFactory Save(MemoryStream memoryStream) { if (this.ShouldProcess) { // Fix the colour palette of gif and png8 images. this.FixIndexedPallete(); if (this.ImageFormat.Equals(ImageFormat.Jpeg)) { // Jpegs can be saved with different settings to include a quality setting for the JPEG compression. // This improves output compression and quality. using (EncoderParameters encoderParameters = ImageUtils.GetEncodingParameters(this.JpegQuality)) { ImageCodecInfo imageCodecInfo = ImageCodecInfo.GetImageEncoders().FirstOrDefault( ici => ici.MimeType.Equals(this.MimeType, StringComparison.OrdinalIgnoreCase)); if (imageCodecInfo != null) { this.Image.Save(memoryStream, imageCodecInfo, encoderParameters); } } } else { this.Image.Save(memoryStream, this.ImageFormat); } } return this; } #region IDisposable Members /// /// Disposes the object and frees resources for the Garbage Collector. /// public void Dispose() { this.Dispose(true); // This object will be cleaned up by the Dispose method. // Therefore, you should call GC.SupressFinalize to // take this object off the finalization queue // and prevent finalization code for this object // from executing a second time. GC.SuppressFinalize(this); } /// /// Disposes the object and frees resources for the Garbage Collector. /// /// If true, the object gets disposed. protected virtual void Dispose(bool disposing) { if (this.isDisposed) { return; } if (disposing) { // Dispose of any managed resources here. if (this.Image != null) { // Dispose of the memory stream from Load and the image. if (this.inputStream != null) { this.inputStream.Dispose(); this.inputStream = null; } this.Image.Dispose(); this.Image = null; } } // Call the appropriate methods to clean up // unmanaged resources here. // Note disposing is done. this.isDisposed = true; } #endregion /// /// Uses the /// to fix the color palette of gif images. /// private void FixIndexedPallete() { // Fix the colour palette of indexed images. if (this.isIndexed) { this.Image = ColorQuantizer.Quantize(this.Image, PixelFormat.Format8bppIndexed); } } #endregion } }