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 @@
+