// -------------------------------------------------------------------------------------------------------------------- // // 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.Concurrent; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using ImageProcessor.Common.Exceptions; using ImageProcessor.Imaging; using ImageProcessor.Imaging.EdgeDetection; using ImageProcessor.Imaging.Filters; using ImageProcessor.Imaging.Formats; using ImageProcessor.Processors; #endregion /// /// Encapsulates methods for processing image files. /// public class ImageFactory : IDisposable { #region Fields /// /// The default quality for image files. /// private const int DefaultQuality = 90; /// /// The backup supported image format. /// private ISupportedImageFormat backupFormat; /// /// 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 Constructors /// /// Initializes a new instance of the class. /// /// /// Whether to preserve exif metadata. Defaults to false. /// public ImageFactory(bool preserveExifData = false) { this.PreserveExifData = preserveExifData; this.ExifPropertyItems = new ConcurrentDictionary(); } #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 path to the local image for manipulation. /// public string ImagePath { get; private set; } /// /// Gets a value indicating whether the image factory should process the file. /// public bool ShouldProcess { get; private set; } /// /// Gets the supported image format. /// public ISupportedImageFormat CurrentImageFormat { get; private set; } /// /// Gets or sets a value indicating whether to preserve exif metadata. /// public bool PreserveExifData { get; set; } /// /// Gets or sets the exif property items. /// public ConcurrentDictionary ExifPropertyItems { get; set; } /// /// Gets or the local image for manipulation. /// public Image Image { get; internal set; } /// /// Gets or sets the stream for storing any input stream to prevent disposal. /// internal Stream InputStream { 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(Stream stream) { ISupportedImageFormat format = FormatUtilities.GetFormat(stream); if (format == null) { throw new ImageFormatException("Input stream is not a supported format."); } // Set our image as the memory stream value. this.Image = format.Load(stream); // Store the stream so we can dispose of it later. this.InputStream = stream; // Set the other properties. format.Quality = DefaultQuality; format.IsIndexed = FormatUtilities.IsIndexed(this.Image); this.backupFormat = format; this.CurrentImageFormat = format; // Always load the data. // TODO. Some custom data doesn't seem to get copied by default methods. foreach (int id in this.Image.PropertyIdList) { this.ExifPropertyItems[id] = this.Image.GetPropertyItem(id); } 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) { FileInfo fileInfo = new FileInfo(imagePath); if (fileInfo.Exists) { this.ImagePath = imagePath; // Open a file stream to prevent the need for lock. using (FileStream fileStream = new FileStream(imagePath, FileMode.Open, FileAccess.Read)) { ISupportedImageFormat format = FormatUtilities.GetFormat(fileStream); if (format == null) { throw new ImageFormatException("Input stream is not a supported format."); } 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 = format.Load(memoryStream); // Store the stream so we can dispose of it later. this.InputStream = memoryStream; // Set the other properties. format.Quality = DefaultQuality; format.IsIndexed = FormatUtilities.IsIndexed(this.Image); this.backupFormat = format; this.CurrentImageFormat = format; // Always load the data. foreach (PropertyItem propertyItem in this.Image.PropertyItems) { this.ExifPropertyItems[propertyItem.Id] = propertyItem; } this.ShouldProcess = true; } } else { throw new FileNotFoundException(imagePath); } 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. #if !__MonoCS__ Image newImage = Image.FromStream(this.InputStream, true); #else Image newImage = Image.FromStream(this.InputStream); #endif // Dispose and reassign the image. this.Image.Dispose(); this.Image = newImage; // Set the other properties. this.CurrentImageFormat = this.backupFormat; this.CurrentImageFormat.Quality = DefaultQuality; } return this; } #region Manipulation /// /// 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.CurrentImageFormat.ApplyProcessor(alpha.ProcessImage, this); } return this; } /// /// Performs auto-rotation to ensure that EXIF defined rotation is reflected in /// the final image. /// /// /// The current instance of the class. /// public ImageFactory AutoRotate() { if (this.ShouldProcess) { AutoRotate autoRotate = new AutoRotate(); this.CurrentImageFormat.ApplyProcessor(autoRotate.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.CurrentImageFormat.ApplyProcessor(brightness.ProcessImage, this); } return this; } /// /// Changes the background color of the current image. /// /// /// The to paint the image with. /// /// /// The current instance of the class. /// public ImageFactory BackgroundColor(Color color) { if (this.ShouldProcess) { BackgroundColor backgroundColor = new BackgroundColor { DynamicParameter = color }; this.CurrentImageFormat.ApplyProcessor(backgroundColor.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, 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.CurrentImageFormat.ApplyProcessor(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) { CropLayer cropLayer = new CropLayer(rectangle.Left, rectangle.Top, rectangle.Width, rectangle.Height, CropMode.Pixels); return this.Crop(cropLayer); } return this; } /// /// Crops the current image to the given location and size. /// /// /// The containing the coordinates and mode to crop the image with. /// /// /// The current instance of the class. /// public ImageFactory Crop(CropLayer cropLayer) { if (this.ShouldProcess) { Crop crop = new Crop { DynamicParameter = cropLayer }; this.CurrentImageFormat.ApplyProcessor(crop.ProcessImage, this); } return this; } /// /// Detects the edges in the current image. /// /// /// The to detect edges with. /// /// /// Whether to convert the image to greyscale first - Defaults to true. /// /// /// The current instance of the class. /// public ImageFactory DetectEdges(IEdgeFilter filter, bool greyscale = true) { if (this.ShouldProcess) { DetectEdges detectEdges = new DetectEdges { DynamicParameter = new Tuple(filter, greyscale) }; this.CurrentImageFormat.ApplyProcessor(detectEdges.ProcessImage, this); } return this; } /// /// Applies a filter to the current image. Use the class to /// assign the correct filter. /// /// /// The of the filter to add to the image. /// /// /// The current instance of the class. /// public ImageFactory Filter(IMatrixFilter matrixFilter) { if (this.ShouldProcess) { Filter filter = new Filter { DynamicParameter = matrixFilter }; this.CurrentImageFormat.ApplyProcessor(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 = false) { if (this.ShouldProcess) { RotateFlipType rotateFlipType = flipVertically ? RotateFlipType.RotateNoneFlipY : RotateFlipType.RotateNoneFlipX; Flip flip = new Flip { DynamicParameter = rotateFlipType }; this.CurrentImageFormat.ApplyProcessor(flip.ProcessImage, this); } return this; } /// /// Sets the output format of the current image to the matching . /// /// The . to set the image to. /// /// The current instance of the class. /// public ImageFactory Format(ISupportedImageFormat format) { if (this.ShouldProcess) { this.CurrentImageFormat = format; } 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); return this.GaussianBlur(layer); } 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.CurrentImageFormat.ApplyProcessor(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); return this.GaussianSharpen(layer); } 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.CurrentImageFormat.ApplyProcessor(gaussianSharpen.ProcessImage, this); } return this; } /// /// Alters the hue of the current image changing the overall color. /// /// /// The angle by which to alter the images hue. /// Any integer between 0 and 360. /// /// /// Whether to rotate the hue of the current image altering each color /// /// /// The current instance of the class. /// public ImageFactory Hue(int degrees, bool rotate = false) { // Sanitize the input. if (degrees > 360 || degrees < 0) { degrees = 0; } if (this.ShouldProcess && degrees > 0) { Hue hue = new Hue { DynamicParameter = new Tuple(degrees, rotate) }; this.CurrentImageFormat.ApplyProcessor(hue.ProcessImage, this); } return this; } /// /// Pixelates an image with the given size. /// /// /// The size of the pixels to create. /// /// The area in which to pixelate the image. If not set, the whole image is pixelated. /// /// /// The current instance of the class. /// public ImageFactory Pixelate(int pixelSize, Rectangle? rectangle = null) { if (this.ShouldProcess && pixelSize > 0) { Pixelate pixelate = new Pixelate { DynamicParameter = new Tuple(pixelSize, rectangle) }; this.CurrentImageFormat.ApplyProcessor(pixelate.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 (percentage <= 100 && percentage >= 0 && this.ShouldProcess) { this.CurrentImageFormat.Quality = percentage; } return this; } /// /// Replaces a color within the current image. /// /// /// The target . /// /// /// The replacement . /// /// /// A value between 0 and 100 with which to alter the target detection accuracy. /// /// /// The . /// public ImageFactory ReplaceColor(Color target, Color replacement, int fuzziness = 0) { // Sanitize the input. if (fuzziness < 0 || fuzziness > 100) { fuzziness = 0; } if (this.ShouldProcess && target != Color.Empty && replacement != Color.Empty) { ReplaceColor replaceColor = new ReplaceColor { DynamicParameter = new Tuple(target, replacement, fuzziness) }; this.CurrentImageFormat.ApplyProcessor(replaceColor.ProcessImage, this); } 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; ResizeLayer resizeLayer = new ResizeLayer(new Size(width, height)); return this.Resize(resizeLayer); } 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) { Dictionary resizeSettings = new Dictionary { { "MaxWidth", resizeLayer.Size.Width.ToString("G") }, { "MaxHeight", resizeLayer.Size.Height.ToString("G") } }; Resize resize = new Resize { DynamicParameter = resizeLayer, Settings = resizeSettings }; this.CurrentImageFormat.ApplyProcessor(resize.ProcessImage, this); } return this; } /// /// Rotates the current image by the given angle. /// /// /// The angle at which to rotate the image in degrees. /// /// /// The current instance of the class. /// public ImageFactory Rotate(int degrees) { if (this.ShouldProcess) { // Sanitize the input. if (degrees > 360 || degrees < 0) { degrees = 0; } Rotate rotate = new Rotate { DynamicParameter = degrees }; this.CurrentImageFormat.ApplyProcessor(rotate.ProcessImage, this); } return this; } /// /// Adds rounded corners to the current image. /// /// /// The radius at which the corner will be rounded. /// /// /// The current instance of the class. /// public ImageFactory RoundedCorners(int radius) { if (this.ShouldProcess) { if (radius < 0) { radius = 0; } RoundedCornerLayer roundedCornerLayer = new RoundedCornerLayer(radius); RoundedCorners roundedCorners = new RoundedCorners { DynamicParameter = roundedCornerLayer }; this.CurrentImageFormat.ApplyProcessor(roundedCorners.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.CurrentImageFormat.ApplyProcessor(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.CurrentImageFormat.ApplyProcessor(saturate.ProcessImage, this); } return this; } /// /// Tints the current image with the given color. /// /// /// The to tint the image with. /// /// /// The current instance of the class. /// public ImageFactory Tint(Color color) { if (this.ShouldProcess) { Tint tint = new Tint { DynamicParameter = color }; this.CurrentImageFormat.ApplyProcessor(tint.ProcessImage, this); } return this; } /// /// Adds a vignette image effect to the current image. /// /// /// The to tint the image with. Defaults to black. /// /// /// The current instance of the class. /// public ImageFactory Vignette(Color? color = null) { if (this.ShouldProcess) { Vignette vignette = new Vignette { DynamicParameter = color.HasValue && !color.Equals(Color.Transparent) ? color.Value : Color.Black }; this.CurrentImageFormat.ApplyProcessor(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.CurrentImageFormat.ApplyProcessor(watermark.ProcessImage, this); } return this; } #endregion /// /// Saves the current image to the specified file path. If the extension does not /// match the correct extension for the current format it will be replaced by the /// correct default value. /// /// The path to save the image to. /// /// The current instance of the class. /// public ImageFactory Save(string filePath) { if (this.ShouldProcess) { // ReSharper disable once AssignNullToNotNullAttribute DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(filePath)); if (!directoryInfo.Exists) { directoryInfo.Create(); } this.Image = this.CurrentImageFormat.Save(filePath, this.Image); } 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(Stream stream) { if (this.ShouldProcess) { this.Image = this.CurrentImageFormat.Save(stream, this.Image); } 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 #endregion } }