diff --git a/src/ImageProcessor/Common/Extensions/ComparableExtensions.cs b/src/ImageProcessor/Common/Extensions/ComparableExtensions.cs
index 36fa82d57a..d223aec673 100644
--- a/src/ImageProcessor/Common/Extensions/ComparableExtensions.cs
+++ b/src/ImageProcessor/Common/Extensions/ComparableExtensions.cs
@@ -42,5 +42,38 @@ namespace ImageProcessor
return value;
}
+
+ ///
+ /// Converts an to a first restricting the value between the
+ /// minimum and maximum allowable ranges.
+ ///
+ /// The this method extends.
+ /// The
+ public static byte ToByte(this int value)
+ {
+ return (byte)value.Clamp(0, 255);
+ }
+
+ ///
+ /// Converts an to a first restricting the value between the
+ /// minimum and maximum allowable ranges.
+ ///
+ /// The this method extends.
+ /// The
+ public static byte ToByte(this float value)
+ {
+ return (byte)value.Clamp(0, 255);
+ }
+
+ ///
+ /// Converts an to a first restricting the value between the
+ /// minimum and maximum allowable ranges.
+ ///
+ /// The this method extends.
+ /// The
+ public static byte ToByte(this double value)
+ {
+ return (byte)value.Clamp(0, 255);
+ }
}
}
diff --git a/src/ImageProcessor/Common/Helpers/Guard.cs b/src/ImageProcessor/Common/Helpers/Guard.cs
index d832e1b82c..81e10700fa 100644
--- a/src/ImageProcessor/Common/Helpers/Guard.cs
+++ b/src/ImageProcessor/Common/Helpers/Guard.cs
@@ -20,7 +20,7 @@ namespace ImageProcessor
internal static class Guard
{
///
- /// Verifies, that the method parameter with specified object value is not null
+ /// Verifies, that the method parameter with specified object value is not null
/// and throws an exception if it is found to be so.
///
///
@@ -50,7 +50,7 @@ namespace ImageProcessor
///
/// Verifies, that the string method parameter with specified object value and message
- /// is not null, not empty and does not contain only blanks and throws an exception
+ /// is not null, not empty and does not contain only blanks and throws an exception
/// if the object is null.
///
/// The target string, which should be checked against being null or empty.
@@ -86,7 +86,8 @@ namespace ImageProcessor
///
/// is greater than the maximum value.
///
- public static void MustBeLessThan(TValue value, TValue max, string parameterName) where TValue : IComparable
+ public static void MustBeLessThan(TValue value, TValue max, string parameterName)
+ where TValue : IComparable
{
if (value.CompareTo(max) >= 0)
{
@@ -107,7 +108,8 @@ namespace ImageProcessor
///
/// is greater than the maximum value.
///
- public static void MustBeLessThanOrEqualTo(TValue value, TValue max, string parameterName) where TValue : IComparable
+ public static void MustBeLessThanOrEqualTo(TValue value, TValue max, string parameterName)
+ where TValue : IComparable
{
if (value.CompareTo(max) > 0)
{
@@ -128,7 +130,8 @@ namespace ImageProcessor
///
/// is less than the minimum value.
///
- public static void MustBeGreaterThan(TValue value, TValue min, string parameterName) where TValue : IComparable
+ public static void MustBeGreaterThan(TValue value, TValue min, string parameterName)
+ where TValue : IComparable
{
if (value.CompareTo(min) <= 0)
{
@@ -149,7 +152,8 @@ namespace ImageProcessor
///
/// is less than the minimum value.
///
- public static void MustBeGreaterThanOrEqualTo(TValue value, TValue min, string parameterName) where TValue : IComparable
+ public static void MustBeGreaterThanOrEqualTo(TValue value, TValue min, string parameterName)
+ where TValue : IComparable
{
if (value.CompareTo(min) < 0)
{
@@ -171,7 +175,8 @@ namespace ImageProcessor
///
/// is less than the minimum value of greater than the maximum value.
///
- public static void MustBeBetweenOrEqualTo(TValue value, TValue min, TValue max, string parameterName) where TValue : IComparable
+ public static void MustBeBetweenOrEqualTo(TValue value, TValue min, TValue max, string parameterName)
+ where TValue : IComparable
{
if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0)
{
diff --git a/src/ImageProcessor/Common/Helpers/PixelOperations.cs b/src/ImageProcessor/Common/Helpers/PixelOperations.cs
new file mode 100644
index 0000000000..804236975c
--- /dev/null
+++ b/src/ImageProcessor/Common/Helpers/PixelOperations.cs
@@ -0,0 +1,141 @@
+//
+// Copyright © James South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessor
+{
+ using System;
+
+ ///
+ /// Performs per-pixel operations.
+ ///
+ public static class PixelOperations
+ {
+ ///
+ /// The array of bytes representing each possible value of color component
+ /// converted from sRGB to the linear color space.
+ ///
+ private static readonly Lazy LinearBytes = new Lazy(GetLinearBytes);
+
+ ///
+ /// The array of bytes representing each possible value of color component
+ /// converted from linear to the sRGB color space.
+ ///
+ private static readonly Lazy SrgbBytes = new Lazy(GetSrgbBytes);
+
+ ///
+ /// Converts an pixel from an sRGB color-space to the equivalent linear color-space.
+ ///
+ ///
+ /// The to convert.
+ ///
+ ///
+ /// The .
+ ///
+ public static Bgra ToLinear(Bgra composite)
+ {
+ // Create only once and lazily.
+ byte[] ramp = LinearBytes.Value;
+
+ return new Bgra(composite.B, ramp[composite.G], ramp[composite.R], ramp[composite.A]);
+ }
+
+ ///
+ /// Converts a pixel from a linear color-space to the equivalent sRGB color-space.
+ ///
+ ///
+ /// The to convert.
+ ///
+ ///
+ /// The .
+ ///
+ public static Bgra ToSrgb(Bgra linear)
+ {
+ // Create only once and lazily.
+ byte[] ramp = SrgbBytes.Value;
+
+ return new Bgra(linear.B, ramp[linear.G], ramp[linear.R], ramp[linear.A]);
+ }
+
+ ///
+ /// Gets an array of bytes representing each possible value of color component
+ /// converted from sRGB to the linear color space.
+ ///
+ ///
+ /// The .
+ ///
+ private static byte[] GetLinearBytes()
+ {
+ byte[] ramp = new byte[256];
+ for (int x = 0; x < 256; ++x)
+ {
+ byte val = (byte)(255f * SrgbToLinear(x / 255f)).Clamp(0, 255);
+ ramp[x] = val;
+ }
+
+ return ramp;
+ }
+
+ ///
+ /// Gets an array of bytes representing each possible value of color component
+ /// converted from linear to the sRGB color space.
+ ///
+ ///
+ /// The .
+ ///
+ private static byte[] GetSrgbBytes()
+ {
+ byte[] ramp = new byte[256];
+ for (int x = 0; x < 256; ++x)
+ {
+ byte val = (byte)(255f * LinearToSrgb(x / 255f)).Clamp(0, 255);
+ ramp[x] = val;
+ }
+
+ return ramp;
+ }
+
+ ///
+ /// Gets the correct linear value from an sRGB signal.
+ ///
+ ///
+ ///
+ /// The signal value to convert.
+ ///
+ /// The .
+ ///
+ private static float SrgbToLinear(float signal)
+ {
+ float a = 0.055f;
+
+ if (signal <= 0.04045)
+ {
+ return signal / 12.92f;
+ }
+
+ return (float)Math.Pow((signal + a) / (1 + a), 2.4);
+ }
+
+ ///
+ /// Gets the correct sRGB value from an linear signal.
+ ///
+ ///
+ ///
+ /// The signal value to convert.
+ ///
+ /// The .
+ ///
+ private static float LinearToSrgb(float signal)
+ {
+ float a = 0.055f;
+
+ if (signal <= 0.0031308)
+ {
+ return signal * 12.92f;
+ }
+
+ return ((float)((1 + a) * Math.Pow(signal, 1 / 2.4f))) - a;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageProcessor/Filters/Contrast.cs b/src/ImageProcessor/Filters/Contrast.cs
index 30823a8f52..076f0dfa53 100644
--- a/src/ImageProcessor/Filters/Contrast.cs
+++ b/src/ImageProcessor/Filters/Contrast.cs
@@ -30,6 +30,11 @@ namespace ImageProcessor.Filters
///
public int Value { get; }
+ protected override void Apply(ImageBase source, ImageBase target, Rectangle sourceRectangle, Rectangle targetRectangle, int startY, int endY)
+ {
+ throw new NotImplementedException();
+ }
+
///
protected override void Apply(ImageBase target, ImageBase source, Rectangle rectangle, int startY, int endY)
{
diff --git a/src/ImageProcessor/Formats/Bmp/BmpDecoderCore.cs b/src/ImageProcessor/Formats/Bmp/BmpDecoderCore.cs
index 2b76fb8923..14f1a12dc5 100644
--- a/src/ImageProcessor/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageProcessor/Formats/Bmp/BmpDecoderCore.cs
@@ -133,7 +133,9 @@ namespace ImageProcessor.Formats
}
else if (this.infoHeader.BitsPerPixel <= 8)
{
- this.ReadRgbPalette(imageData, palette,
+ this.ReadRgbPalette(
+ imageData,
+ palette,
this.infoHeader.Width,
this.infoHeader.Height,
this.infoHeader.BitsPerPixel);
diff --git a/src/ImageProcessor/IImageProcessor.cs b/src/ImageProcessor/IImageProcessor.cs
index 667f1ae30b..a9eea7dbd0 100644
--- a/src/ImageProcessor/IImageProcessor.cs
+++ b/src/ImageProcessor/IImageProcessor.cs
@@ -29,5 +29,7 @@ namespace ImageProcessor
/// doesnt fit the dimension of the image.
///
void Apply(ImageBase target, ImageBase source, Rectangle rectangle);
+
+ void Apply(ImageBase target, ImageBase source, int width, int height, Rectangle targetRectangle, Rectangle sourceRectangle);
}
}
diff --git a/src/ImageProcessor/ImageExtensions.cs b/src/ImageProcessor/ImageExtensions.cs
index 2298f8785e..395dbb8621 100644
--- a/src/ImageProcessor/ImageExtensions.cs
+++ b/src/ImageProcessor/ImageExtensions.cs
@@ -76,6 +76,29 @@ namespace ImageProcessor
return source;
}
+ ///
+ /// Applies the collection of processors to the image.
+ ///
+ /// The image this method extends.
+ ///
+ /// The rectangle defining the bounds of the pixels the image filter with adjust.
+ ///
+ ///
+ ///
+ ///
+ /// Any processors to apply to the image.
+ /// The .
+ public static Image Process(this Image source, int width, int height, Rectangle sourceRectangle, Rectangle targetRectangle, params IImageProcessor[] processors)
+ {
+ // ReSharper disable once LoopCanBeConvertedToQuery
+ foreach (IImageProcessor filter in processors)
+ {
+ source = PerformAction(source, false, (sourceImage, targetImage) => filter.Apply(targetImage, sourceImage, width, height, targetRectangle, sourceRectangle));
+ }
+
+ return source;
+ }
+
///
/// Performs the given action on the source image.
///
diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj
index 8e779c1c2f..2ec0e8c088 100644
--- a/src/ImageProcessor/ImageProcessor.csproj
+++ b/src/ImageProcessor/ImageProcessor.csproj
@@ -38,9 +38,9 @@
-
+
@@ -179,6 +179,10 @@
+
+
+
+
diff --git a/src/ImageProcessor/ParallelImageProcessor.cs b/src/ImageProcessor/ParallelImageProcessor.cs
index 3bc28e15df..b1533cf65f 100644
--- a/src/ImageProcessor/ParallelImageProcessor.cs
+++ b/src/ImageProcessor/ParallelImageProcessor.cs
@@ -50,6 +50,51 @@ namespace ImageProcessor
}
}
+ ///
+ public void Apply(ImageBase target, ImageBase source, int width, int height, Rectangle targetRectangle = default(Rectangle), Rectangle sourceRectangle = default(Rectangle))
+ {
+ this.OnApply();
+
+ byte[] pixels = new byte[width * height * 4];
+ target.SetPixels(width, height, pixels);
+
+ if (targetRectangle == Rectangle.Empty)
+ {
+ targetRectangle = target.Bounds;
+ }
+
+ if (sourceRectangle == Rectangle.Empty)
+ {
+ sourceRectangle = source.Bounds;
+ }
+
+ 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 = targetRectangle.Bottom / partitionCount;
+ int yStart = current * batchSize;
+ int yEnd = current == partitionCount - 1 ? targetRectangle.Bottom : yStart + batchSize;
+
+ this.Apply(target, source, targetRectangle, sourceRectangle, yStart, yEnd);
+ });
+ }
+
+ Task.WaitAll(tasks);
+ }
+ else
+ {
+ this.Apply(target, source, targetRectangle, sourceRectangle, targetRectangle.Y, targetRectangle.Bottom);
+ }
+ }
+
///
/// This method is called before the process is applied to prepare the processor.
///
@@ -57,6 +102,8 @@ namespace ImageProcessor
{
}
+ protected abstract void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY);
+
///
/// Apply a process to an image to alter the pixels at the area of the specified rectangle.
///
diff --git a/src/ImageProcessor/Samplers/BicubicResampler.cs b/src/ImageProcessor/Samplers/BicubicResampler.cs
new file mode 100644
index 0000000000..4c0d285af5
--- /dev/null
+++ b/src/ImageProcessor/Samplers/BicubicResampler.cs
@@ -0,0 +1,42 @@
+//
+// Copyright © James South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessor.Samplers
+{
+ ///
+ /// The function implements the bicubic kernel algorithm W(x) as described on
+ /// Wikipedia
+ ///
+ public class BicubicResampler : IResampler
+ {
+ ///
+ public double Radius => 4;
+
+ ///
+ public double GetValue(double x)
+ {
+ // The coefficient.
+ double a = -0.5;
+
+ if (x < 0)
+ {
+ x = -x;
+ }
+
+ double result = 0;
+
+ if (x <= 1)
+ {
+ result = (((1.5 * x) - 2.5) * x * x) + 1;
+ }
+ else if (x < 2)
+ {
+ result = (((((a * x) + 2.5) * x) - 4) * x) + 2;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/ImageProcessor/Samplers/IResampler.cs b/src/ImageProcessor/Samplers/IResampler.cs
new file mode 100644
index 0000000000..37ee3c9ca1
--- /dev/null
+++ b/src/ImageProcessor/Samplers/IResampler.cs
@@ -0,0 +1,22 @@
+namespace ImageProcessor.Samplers
+{
+ ///
+ /// Encasulates an interpolation algorithm for resampling images.
+ ///
+ public interface IResampler
+ {
+ ///
+ /// Gets the radius in which to sample pixels.
+ ///
+ double Radius { get; }
+
+ ///
+ /// Gets the result of the interpolation algorithm.
+ ///
+ /// The value to process.
+ ///
+ /// The
+ ///
+ double GetValue(double x);
+ }
+}
diff --git a/src/ImageProcessor/Samplers/ImageSampleExtensions.cs b/src/ImageProcessor/Samplers/ImageSampleExtensions.cs
new file mode 100644
index 0000000000..60ee861030
--- /dev/null
+++ b/src/ImageProcessor/Samplers/ImageSampleExtensions.cs
@@ -0,0 +1,28 @@
+//
+// Copyright © James South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessor.Samplers
+{
+ ///
+ /// Exstensions methods for to apply samplers to the image.
+ ///
+ public static class ImageSampleExtensions
+ {
+ public static Image Resize(this Image source, int width, int height)
+ {
+ return source.Process(width, height, default(Rectangle), default(Rectangle), new Resize(new BicubicResampler(), width, height));
+ }
+
+ public static Image Resize(this Image source, int width, int height, IResampler sampler)
+ {
+ return source.Process(width, height, default(Rectangle), default(Rectangle), new Resize(sampler, width, height));
+ }
+
+ public static Image Resize(this Image source, int width, int height, IResampler sampler, Rectangle sourceRectangle, Rectangle targetRectangle)
+ {
+ return source.Process(width, height, sourceRectangle, targetRectangle, new Resize(sampler, width, height));
+ }
+ }
+}
diff --git a/src/ImageProcessor/Samplers/Resize.cs b/src/ImageProcessor/Samplers/Resize.cs
new file mode 100644
index 0000000000..48e9618281
--- /dev/null
+++ b/src/ImageProcessor/Samplers/Resize.cs
@@ -0,0 +1,149 @@
+using System;
+
+namespace ImageProcessor.Samplers
+{
+ public class Resize : ParallelImageProcessor
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The sampler to perform the resize operation.
+ ///
+ public Resize(IResampler sampler, int width, int height)
+ {
+ Guard.NotNull(sampler, nameof(sampler));
+ Guard.MustBeGreaterThan(width, 0, nameof(width));
+ Guard.MustBeGreaterThan(height, 0, nameof(height));
+
+ this.Sampler = sampler;
+ this.Width = width;
+ this.Height = height;
+ }
+
+ ///
+ /// Gets the sampler to perform the resize operation.
+ ///
+ public IResampler Sampler { get; }
+
+ ///
+ /// Gets the width.
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets the height.
+ ///
+ public int Height { get; }
+
+ ///
+ protected override void Apply(
+ ImageBase target,
+ ImageBase source,
+ Rectangle targetRectangle,
+ Rectangle sourceRectangle,
+ int startY,
+ int endY)
+ {
+ int sourceWidth = source.Width;
+ int sourceHeight = source.Height;
+
+ int width = target.Width;
+ int height = target.Height;
+
+ int startX = targetRectangle.X;
+ int endX = targetRectangle.Right;
+ int right = (int)(this.Sampler.Radius + .5);
+ int left = -right;
+
+ // Scaling factors
+ double widthFactor = sourceWidth / (double)targetRectangle.Width;
+ double heightFactor = sourceHeight / (double)targetRectangle.Height;
+
+ // Width and height decreased by 1
+ int maxHeight = sourceHeight - 1;
+ int maxWidth = sourceWidth - 1;
+
+ for (int y = startY; y < endY; y++)
+ {
+ if (y >= 0 && y < height)
+ {
+ // Y coordinates of source points.
+ double originY = ((startY - targetRectangle.Y) * heightFactor) - 0.5;
+ int originY1 = (int)originY;
+ double dy = originY - originY1;
+
+ // For each row.
+ for (int x = startX; x < endX; x++)
+ {
+ if (x >= 0 && x < width)
+ {
+ // X coordinates of source points.
+ double originX = ((x - startX) * widthFactor) - 0.5f;
+ int originX1 = (int)originX;
+ double dx = originX - originX1;
+
+ // Destination color components
+ double r = 0;
+ double g = 0;
+ double b = 0;
+ double a = 0;
+
+ for (int yy = left; yy < right; yy++)
+ {
+ // Get Y cooefficient
+ double kernel1 = this.Sampler.GetValue(dy - yy);
+
+ int originY2 = originY1 + yy;
+ if (originY2 < 0)
+ {
+ originY2 = 0;
+ }
+
+ if (originY2 > maxHeight)
+ {
+ originY2 = maxHeight;
+ }
+
+ for (int xx = left; xx < right; xx++)
+ {
+ // Get X cooefficient
+ double kernel2 = kernel1 * this.Sampler.GetValue(xx - dx);
+
+ int originX2 = originX1 + xx;
+ if (originX2 < 0)
+ {
+ originX2 = 0;
+ }
+
+ if (originX2 > maxWidth)
+ {
+ originX2 = maxWidth;
+ }
+
+ Bgra sourceColor = source[originX2, originY2];
+ sourceColor = PixelOperations.ToLinear(sourceColor);
+
+ r += kernel2 * sourceColor.R;
+ g += kernel2 * sourceColor.G;
+ b += kernel2 * sourceColor.B;
+ a += kernel2 * sourceColor.A;
+ }
+ }
+
+ Bgra destinationColor = new Bgra(b.ToByte(), g.ToByte(), r.ToByte(), a.ToByte());
+ destinationColor = PixelOperations.ToSrgb(destinationColor);
+ target[x, y] = destinationColor;
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ protected override void Apply(ImageBase target, ImageBase source, Rectangle rectangle, int startY, int endY)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/tests/ImageProcessor.Tests/Filters/FilterTests.cs b/tests/ImageProcessor.Tests/Filters/FilterTests.cs
index a6452d37ac..675bc3815d 100644
--- a/tests/ImageProcessor.Tests/Filters/FilterTests.cs
+++ b/tests/ImageProcessor.Tests/Filters/FilterTests.cs
@@ -6,6 +6,7 @@ namespace ImageProcessor.Tests.Filters
using System.IO;
using ImageProcessor.Filters;
+ using ImageProcessor.Samplers;
using Xunit;
@@ -14,8 +15,8 @@ namespace ImageProcessor.Tests.Filters
public static readonly List Files = new List
{
{ "../../TestImages/Formats/Jpg/Backdrop.jpg"},
- { "../../TestImages/Formats/Bmp/Car.bmp" },
- { "../../TestImages/Formats/Png/cmyk.png" },
+ //{ "../../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" },
@@ -54,5 +55,30 @@ namespace ImageProcessor.Tests.Filters
}
}
}
+
+ [Fact]
+ public void ResizeImage()
+ {
+ if (!Directory.Exists("Resized"))
+ {
+ Directory.CreateDirectory("Resized");
+ }
+
+ foreach (string file in Files)
+ {
+ using (FileStream stream = File.OpenRead(file))
+ {
+ Stopwatch watch = Stopwatch.StartNew();
+ Image image = new Image(stream);
+ string filename = Path.GetFileName(file);
+ using (FileStream output = File.OpenWrite($"Resized/{ Path.GetFileName(filename) }"))
+ {
+ image.Resize(400, 400).Save(output);
+ }
+
+ Trace.WriteLine($"{ filename }: { watch.ElapsedMilliseconds}ms");
+ }
+ }
+ }
}
}