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"); + } + } + } } }