diff --git a/src/ImageProcessor/Filters/Contrast.cs b/src/ImageProcessor/Filters/Contrast.cs new file mode 100644 index 0000000000..752bef0186 --- /dev/null +++ b/src/ImageProcessor/Filters/Contrast.cs @@ -0,0 +1,70 @@ +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor.Filters +{ + using System; + + /// + /// An to change the contrast of an . + /// + public class Contrast : ParallelImageFilter + { + /// + /// Initializes a new instance of the class. + /// + /// The new contrast of the image. Must be between -100 and 100. + /// + /// is less than -100 is greater than 100. + /// + public Contrast(int contrast) + { + Guard.MustBeBetweenOrEqualTo(contrast, -100, 100, nameof(contrast)); + this.Value = contrast; + } + + /// + /// Gets the contrast value. + /// + public int Value { get; } + + /// + protected override void Apply(ImageBase target, ImageBase source, Rectangle rectangle, int startY, int endY) + { + double contrast = (100.0 + this.Value) / 100.0; + + for (int y = startY; y < endY; y++) + { + for (int x = rectangle.X; x < rectangle.Right; x++) + { + Bgra color = source[x, y]; + + double r = color.R / 255.0; + r -= 0.5; + r *= contrast; + r += 0.5; + r *= 255; + r = r.Clamp(0, 255); + + double g = color.G / 255.0; + g -= 0.5; + g *= contrast; + g += 0.5; + g *= 255; + g = g.Clamp(0, 255); + + double b = color.B / 255.0; + b -= 0.5; + b *= contrast; + b += 0.5; + b *= 255; + b = b.Clamp(0, 255); + + target[x, y] = new Bgra((byte)b, (byte)g, (byte)r, color.A); + } + } + } + } +} diff --git a/src/ImageProcessor/Filters/IImageFilter.cs b/src/ImageProcessor/Filters/IImageFilter.cs new file mode 100644 index 0000000000..11843d0217 --- /dev/null +++ b/src/ImageProcessor/Filters/IImageFilter.cs @@ -0,0 +1,34 @@ +namespace ImageProcessor.Filters +{ + /// + /// Image processing filter interface. + /// + /// + /// The interface defines the set of methods, which should be + /// provided by all image processing filters. Methods of this interface + /// manipulate the original image. + /// + public interface IImageFilter + { + /// + /// Apply filter to an image at the area of the specified rectangle. + /// + /// Target image to apply filter to. + /// The source image. Cannot be null. + /// The rectangle, which defines the area of the + /// image where the filter should be applied to. + /// The method keeps the source image unchanged and returns the + /// the result of image processing filter as new image. + /// + /// + /// is null. + /// - or - + /// + /// is null. + /// + /// + /// doesnt fit the dimension of the image. + /// + void Apply(ImageBase target, ImageBase source, Rectangle rectangle); + } +} diff --git a/src/ImageProcessor/Filters/ImageFilterExtensions.cs b/src/ImageProcessor/Filters/ImageFilterExtensions.cs new file mode 100644 index 0000000000..c97500a23c --- /dev/null +++ b/src/ImageProcessor/Filters/ImageFilterExtensions.cs @@ -0,0 +1,81 @@ +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor.Filters +{ + using System; + + /// + /// Exstension methods for performing filtering methods again an image. + /// + public static class ImageFilterExtensions + { + /// + /// Applies the collection of filters to the image. + /// + /// The image this method extends. + /// Any filters to apply to the image. + /// The . + public static Image Filter(this Image source, params IImageFilter[] filters) => Filter(source, source.Bounds, filters); + + /// + /// Applies the collection of filters to the image. + /// + /// The image this method extends. + /// + /// The rectangle defining the bounds of the pixels the image filter with adjust. + /// Any filters to apply to the image. + /// The . + public static Image Filter(this Image source, Rectangle rectangle, params IImageFilter[] filters) + { + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (IImageFilter filter in filters) + { + source = PerformAction(source, true, (sourceImage, targetImage) => filter.Apply(targetImage, sourceImage, rectangle)); + } + + return source; + } + + /// + /// Alters the contrast component of the image. + /// + /// The image this method extends. + /// The new contrast of the image. Must be between -100 and 100. + /// The . + public static Image Contrast(this Image source, int amount) => source.Filter(new Contrast(amount)); + + /// + /// Performs the given action on the source image. + /// + /// The image to perform the action against. + /// Whether to clone the image. + /// The to perform against the image. + /// The . + private static Image PerformAction(Image source, bool clone, Action action) + { + Image transformedImage = clone ? new Image(source) : new Image(source.Width, source.Height); + action(source, transformedImage); + + for (int i = 0; i < source.Frames.Count; i++) + { + ImageFrame frame = source.Frames[i]; + ImageFrame tranformedFrame = new ImageFrame(frame); + action(frame, tranformedFrame); + + if (!clone) + { + transformedImage.Frames.Add(tranformedFrame); + } + else + { + transformedImage.Frames[i] = tranformedFrame; + } + } + + return transformedImage; + } + } +} diff --git a/src/ImageProcessor/Filters/ParallelImageFilter.cs b/src/ImageProcessor/Filters/ParallelImageFilter.cs new file mode 100644 index 0000000000..647e325151 --- /dev/null +++ b/src/ImageProcessor/Filters/ParallelImageFilter.cs @@ -0,0 +1,57 @@ +namespace ImageProcessor.Filters +{ + using System; + using System.Threading.Tasks; + + /// + /// Allows the application of filters using prallel processing. + /// + public abstract class ParallelImageFilter : IImageFilter + { + /// + /// Gets or sets the count of workers to run the filter in parallel. + /// + public int Parallelism { get; set; } = Environment.ProcessorCount; + + /// + public void Apply(ImageBase target, ImageBase source, Rectangle rectangle) + { + this.OnApply(); + + if (this.Parallelism > 1) + { + int partitionCount = this.Parallelism; + + Task[] tasks = new Task[partitionCount]; + + for (int p = 0; p < partitionCount; p++) + { + int current = p; + tasks[p] = Task.Run(() => + { + int batchSize = rectangle.Height / partitionCount; + int yStart = rectangle.Y + (current * batchSize); + int yEnd = current == partitionCount - 1 ? rectangle.Bottom : yStart + batchSize; + + this.Apply(target, source, rectangle, yStart, yEnd); + }); + } + + Task.WaitAll(tasks); + } + else + { + this.Apply(target, source, rectangle, rectangle.Y, rectangle.Bottom); + } + } + + /// + /// This method is called before the filter is applied to prepare the filter. + /// + protected virtual void OnApply() + { + } + + protected abstract void Apply(ImageBase target, ImageBase source, Rectangle rectangle, int startY, int endY); + } +} diff --git a/src/ImageProcessor/Formats/Jpg/JpegDecoder.cs b/src/ImageProcessor/Formats/Jpg/JpegDecoder.cs index 3e96636650..f1aeb814c6 100644 --- a/src/ImageProcessor/Formats/Jpg/JpegDecoder.cs +++ b/src/ImageProcessor/Formats/Jpg/JpegDecoder.cs @@ -1,12 +1,7 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright © James South and contributors. -// Licensed under the Apache License, Version 2.0. +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. // -// -// Image decoder for generating an image out of a jpg stream. -// -// -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Formats { @@ -42,7 +37,11 @@ namespace ImageProcessor.Formats { Guard.NotNullOrEmpty(extension, "extension"); - if (extension.StartsWith(".")) extension = extension.Substring(1); + if (extension.StartsWith(".")) + { + extension = extension.Substring(1); + } + return extension.Equals("JPG", StringComparison.OrdinalIgnoreCase) || extension.Equals("JPEG", StringComparison.OrdinalIgnoreCase) || extension.Equals("JFIF", StringComparison.OrdinalIgnoreCase); @@ -68,7 +67,7 @@ namespace ImageProcessor.Formats if (header.Length >= 11) { bool isJpeg = IsJpeg(header); - bool isExif = IsExif(header); + bool isExif = this.IsExif(header); isSupported = isJpeg || isExif; } @@ -76,30 +75,6 @@ namespace ImageProcessor.Formats return isSupported; } - private bool IsExif(byte[] header) - { - bool isExif = - header[6] == 0x45 && // E - header[7] == 0x78 && // x - header[8] == 0x69 && // i - header[9] == 0x66 && // f - header[10] == 0x00; - - return isExif; - } - - private static bool IsJpeg(byte[] header) - { - bool isJpg = - header[6] == 0x4A && // J - header[7] == 0x46 && // F - header[8] == 0x49 && // I - header[9] == 0x46 && // F - header[10] == 0x00; - - return isJpg; - } - /// /// Decodes the image from the specified stream and sets /// the data to image. @@ -137,7 +112,7 @@ namespace ImageProcessor.Formats { Sample sample = row.GetAt(x); - int offset = (y * pixelWidth + x) * 4; + int offset = ((y * pixelWidth) + x) * 4; pixels[offset + 0] = (byte)sample[2]; pixels[offset + 1] = (byte)sample[1]; @@ -148,5 +123,34 @@ namespace ImageProcessor.Formats image.SetPixels(pixelWidth, pixelHeight, pixels); } + + /// + /// + /// + /// + /// + private bool IsExif(byte[] header) + { + bool isExif = + header[6] == 0x45 && // E + header[7] == 0x78 && // x + header[8] == 0x69 && // i + header[9] == 0x66 && // f + header[10] == 0x00; + + return isExif; + } + + private static bool IsJpeg(byte[] header) + { + bool isJpg = + header[6] == 0x4A && // J + header[7] == 0x46 && // F + header[8] == 0x49 && // I + header[9] == 0x46 && // F + header[10] == 0x00; + + return isJpg; + } } } diff --git a/src/ImageProcessor/Image.cs b/src/ImageProcessor/Image.cs index c2a5c8ebfe..a75364ae2a 100644 --- a/src/ImageProcessor/Image.cs +++ b/src/ImageProcessor/Image.cs @@ -61,6 +61,7 @@ namespace ImageProcessor { this.HorizontalResolution = DefaultHorizontalResolution; this.VerticalResolution = DefaultVerticalResolution; + this.CurrentImageFormat = DefaultFormats.Value.First(f => f.GetType() == typeof(PngFormat)); } /// @@ -74,6 +75,7 @@ namespace ImageProcessor { this.HorizontalResolution = DefaultHorizontalResolution; this.VerticalResolution = DefaultVerticalResolution; + this.CurrentImageFormat = DefaultFormats.Value.First(f => f.GetType() == typeof(PngFormat)); } /// @@ -97,6 +99,7 @@ namespace ImageProcessor this.HorizontalResolution = DefaultHorizontalResolution; this.VerticalResolution = DefaultVerticalResolution; + this.CurrentImageFormat = other.CurrentImageFormat; } /// diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index db3003fcfa..377b7c3635 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -38,9 +38,12 @@ - + + + + @@ -174,6 +177,7 @@ + diff --git a/src/ImageProcessor/Samplers/IImageSampler.cs b/src/ImageProcessor/Samplers/IImageSampler.cs new file mode 100644 index 0000000000..24e815ef6a --- /dev/null +++ b/src/ImageProcessor/Samplers/IImageSampler.cs @@ -0,0 +1,23 @@ +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor.Samplers +{ + /// + /// Encapsulates the methods required for all image sampling (resizing) algorithms. + /// + public interface IImageSampler + { + /// + /// Resizes the specified source image by creating a new image with + /// the specified size which is a resized version of the passed image. + /// + /// The source image. + /// The target image. + /// The width. + /// The height. + void Sample(ImageBase source, ImageBase target, int width, int height); + } +} diff --git a/tests/ImageProcessor.Tests/Filters/FilterTests.cs b/tests/ImageProcessor.Tests/Filters/FilterTests.cs new file mode 100644 index 0000000000..e6b7b4944c --- /dev/null +++ b/tests/ImageProcessor.Tests/Filters/FilterTests.cs @@ -0,0 +1,58 @@ + +namespace ImageProcessor.Tests.Filters +{ + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + + using ImageProcessor.Filters; + + using Xunit; + + public class FilterTests + { + public static readonly List Files = new List + { + { "../../TestImages/Formats/Jpg/Backdrop.jpg"}, + { "../../TestImages/Formats/Bmp/Car.bmp" }, + { "../../TestImages/Formats/Png/cmyk.png" }, + //{ "../../TestImages/Formats/Gif/a.gif" }, + //{ "../../TestImages/Formats/Gif/leaf.gif" }, + //{ "../../TestImages/Formats/Gif/ani.gif" }, + //{ "../../TestImages/Formats/Gif/ani2.gif" }, + //{ "../../TestImages/Formats/Gif/giphy.gif" }, + }; + + public static readonly TheoryData Filters = new TheoryData + { + { "Contrast-50", new Contrast(50) }, + { "Contrast--50", new Contrast(-50) }, + }; + + [Theory] + [MemberData("Filters")] + public void FilterImage(string name, IImageFilter filter) + { + if (!Directory.Exists("Filtered")) + { + Directory.CreateDirectory("Filtered"); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Stopwatch watch = Stopwatch.StartNew(); + Image image = new Image(stream); + string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); + using (FileStream output = File.OpenWrite($"Filtered/{ Path.GetFileName(filename) }")) + { + image.Filter(filter).Save(output); + } + + Trace.WriteLine($"{ name }: { watch.ElapsedMilliseconds}ms"); + } + } + } + } +} diff --git a/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs b/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs index aac27677dd..713adf60dc 100644 --- a/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs +++ b/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs @@ -2,9 +2,8 @@ { using System.Diagnostics; using System.IO; - using System.Linq; - using ImageProcessor.Formats; + using Formats; using Xunit; diff --git a/tests/ImageProcessor.Tests/ImageProcessor.Tests.csproj b/tests/ImageProcessor.Tests/ImageProcessor.Tests.csproj index ec75612e25..7d79e2e92a 100644 --- a/tests/ImageProcessor.Tests/ImageProcessor.Tests.csproj +++ b/tests/ImageProcessor.Tests/ImageProcessor.Tests.csproj @@ -53,6 +53,7 @@ +