diff --git a/src/ImageSharp.Processing/Binarization/Dither.cs b/src/ImageSharp.Processing/Binarization/Dither.cs new file mode 100644 index 000000000..f481ac4df --- /dev/null +++ b/src/ImageSharp.Processing/Binarization/Dither.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp +{ + using System; + + using ImageSharp.Dithering; + using ImageSharp.Processing.Processors; + + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Alters the alpha component of the image. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The . + public static Image Dither(this Image source, IErrorDiffuser diffuser, float threshold) + where TColor : struct, IPackedPixel, IEquatable + { + return Dither(source, diffuser, threshold, source.Bounds); + } + + /// + /// Alters the alpha component of the image. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static Image Dither(this Image source, IErrorDiffuser diffuser, float threshold, Rectangle rectangle) + where TColor : struct, IPackedPixel, IEquatable + { + source.ApplyProcessor(new ErrorDiffusionDitherProcessor(diffuser, threshold), rectangle); + return source; + } + } +} diff --git a/src/ImageSharp.Processing/Processors/Binarization/BinaryThresholdProcessor.cs b/src/ImageSharp.Processing/Processors/Binarization/BinaryThresholdProcessor.cs index 2eb5225f8..cb3758748 100644 --- a/src/ImageSharp.Processing/Processors/Binarization/BinaryThresholdProcessor.cs +++ b/src/ImageSharp.Processing/Processors/Binarization/BinaryThresholdProcessor.cs @@ -20,28 +20,26 @@ namespace ImageSharp.Processing.Processors /// Initializes a new instance of the class. /// /// The threshold to split the image. Must be between 0 and 1. - /// - /// is less than 0 or is greater than 1. - /// public BinaryThresholdProcessor(float threshold) { - // TODO: Check limit. + // TODO: Check thresholding limit. Colors should probably have Max/Min/Middle properties. Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold)); - this.Value = threshold; + this.Threshold = threshold; + // Default to white/black for upper/lower. TColor upper = default(TColor); - upper.PackFromVector4(Color.White.ToVector4()); + upper.PackFromBytes(255, 255, 255, 255); this.UpperColor = upper; TColor lower = default(TColor); - lower.PackFromVector4(Color.Black.ToVector4()); + lower.PackFromBytes(0, 0, 0, 255); this.LowerColor = lower; } /// /// Gets the threshold value. /// - public float Value { get; } + public float Threshold { get; } /// /// Gets or sets the color to use for pixels that are above the threshold. @@ -62,7 +60,7 @@ namespace ImageSharp.Processing.Processors /// protected override void OnApply(ImageBase source, Rectangle sourceRectangle) { - float threshold = this.Value; + float threshold = this.Threshold; TColor upper = this.UpperColor; TColor lower = this.LowerColor; diff --git a/src/ImageSharp.Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs b/src/ImageSharp.Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs new file mode 100644 index 000000000..c5b78b639 --- /dev/null +++ b/src/ImageSharp.Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs @@ -0,0 +1,111 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Processing.Processors +{ + using System; + + using ImageSharp.Dithering; + + /// + /// An that dithers an image using error diffusion. + /// + /// The pixel format. + public class ErrorDiffusionDitherProcessor : ImageProcessor + where TColor : struct, IPackedPixel, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The error diffuser + /// The threshold to split the image. Must be between 0 and 1. + public ErrorDiffusionDitherProcessor(IErrorDiffuser diffuser, float threshold) + { + Guard.NotNull(diffuser, nameof(diffuser)); + + // TODO: Check thresholding limit. Colors should probably have Max/Min/Middle properties. + Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold)); + + this.Diffuser = diffuser; + this.Threshold = threshold; + + // Default to white/black for upper/lower. + TColor upper = default(TColor); + upper.PackFromBytes(255, 255, 255, 255); + this.UpperColor = upper; + + TColor lower = default(TColor); + lower.PackFromBytes(0, 0, 0, 255); + this.LowerColor = lower; + } + + /// + /// Gets the error diffuser. + /// + public IErrorDiffuser Diffuser { get; } + + /// + /// Gets the threshold value. + /// + public float Threshold { get; } + + /// + /// Gets or sets the color to use for pixels that are above the threshold. + /// + public TColor UpperColor { get; set; } + + /// + /// Gets or sets the color to use for pixels that fall below the threshold. + /// + public TColor LowerColor { get; set; } + + /// + protected override void BeforeApply(ImageBase source, Rectangle sourceRectangle) + { + new GrayscaleBt709Processor().Apply(source, sourceRectangle); + } + + /// + protected override void OnApply(ImageBase source, Rectangle sourceRectangle) + { + int startY = sourceRectangle.Y; + int endY = sourceRectangle.Bottom; + int startX = sourceRectangle.X; + int endX = sourceRectangle.Right; + + // Align start/end positions. + int minX = Math.Max(0, startX); + int maxX = Math.Min(source.Width, endX); + int minY = Math.Max(0, startY); + int maxY = Math.Min(source.Height, endY); + + // Reset offset if necessary. + if (minX > 0) + { + startX = 0; + } + + if (minY > 0) + { + startY = 0; + } + + using (PixelAccessor sourcePixels = source.Lock()) + { + for (int y = minY; y < maxY; y++) + { + int offsetY = y - startY; + for (int x = minX; x < maxX; x++) + { + int offsetX = x - startX; + TColor sourceColor = sourcePixels[offsetX, offsetY]; + TColor transformedColor = sourceColor.ToVector4().X >= this.Threshold ? this.UpperColor : this.LowerColor; + this.Diffuser.Dither(sourcePixels, sourceColor, transformedColor, offsetX, offsetY, maxX, maxY); + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 97bd34def..7e47501f3 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -111,26 +111,27 @@ namespace ImageSharp.Tests foreach (TestFile file in Files) { - Image image = file.CreateImage(); - - using (FileStream output = File.OpenWrite($"{path}/{file.FileNameWithoutExtension}.gif")) + using (Image image = file.CreateImage()) { - image.SaveAsGif(output); - } + using (FileStream output = File.OpenWrite($"{path}/{file.FileNameWithoutExtension}.bmp")) + { + image.SaveAsBmp(output); + } - using (FileStream output = File.OpenWrite($"{path}/{file.FileNameWithoutExtension}.bmp")) - { - image.SaveAsBmp(output); - } + using (FileStream output = File.OpenWrite($"{path}/{file.FileNameWithoutExtension}.jpg")) + { + image.SaveAsJpeg(output); + } - using (FileStream output = File.OpenWrite($"{path}/{file.FileNameWithoutExtension}.jpg")) - { - image.SaveAsJpeg(output); - } + using (FileStream output = File.OpenWrite($"{path}/{file.FileNameWithoutExtension}.png")) + { + image.SaveAsPng(output); + } - using (FileStream output = File.OpenWrite($"{path}/{file.FileNameWithoutExtension}.png")) - { - image.SaveAsPng(output); + using (FileStream output = File.OpenWrite($"{path}/{file.FileNameWithoutExtension}.gif")) + { + image.SaveAsGif(output); + } } } } @@ -142,22 +143,25 @@ namespace ImageSharp.Tests foreach (TestFile file in Files) { - Image image = file.CreateImage(); - byte[] serialized; - using (MemoryStream memoryStream = new MemoryStream()) + using (Image image = file.CreateImage()) { - image.Save(memoryStream); - memoryStream.Flush(); - serialized = memoryStream.ToArray(); + using (MemoryStream memoryStream = new MemoryStream()) + { + image.Save(memoryStream); + memoryStream.Flush(); + serialized = memoryStream.ToArray(); + } } using (MemoryStream memoryStream = new MemoryStream(serialized)) { - Image image2 = new Image(memoryStream); - using (FileStream output = File.OpenWrite($"{path}/{file.FileName}")) + using (Image image2 = new Image(memoryStream)) { - image2.Save(output); + using (FileStream output = File.OpenWrite($"{path}/{file.FileName}")) + { + image2.Save(output); + } } } } diff --git a/tests/ImageSharp.Tests/Processors/Filters/DitherTest.cs b/tests/ImageSharp.Tests/Processors/Filters/DitherTest.cs new file mode 100644 index 000000000..3de2481e5 --- /dev/null +++ b/tests/ImageSharp.Tests/Processors/Filters/DitherTest.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System.IO; + + using ImageSharp.Dithering; + + using Xunit; + + public class DitherTest : FileTestBase + { + [Fact] + public void ImageShouldApplyDitherFilter() + { + string path = this.CreateOutputDirectory("Dither"); + + foreach (TestFile file in Files) + { + using (Image image = file.CreateImage()) + using (FileStream output = File.OpenWrite($"{path}/{file.FileName}")) + { + image.Dither(new SierraLite(), .5F).Save(output); + } + } + } + + [Fact] + public void ImageShouldApplyDitherFilterInBox() + { + string path = this.CreateOutputDirectory("Dither"); + + foreach (TestFile file in Files) + { + string filename = file.GetFileName("-InBox"); + using (Image image = file.CreateImage()) + using (FileStream output = File.OpenWrite($"{path}/{filename}")) + { + image.Dither(new SierraLite(), .5F, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output); + } + } + } + } +} \ No newline at end of file