diff --git a/src/ImageProcessor.Playground/Program.cs b/src/ImageProcessor.Playground/Program.cs index 295e72a1ed..c290f27482 100644 --- a/src/ImageProcessor.Playground/Program.cs +++ b/src/ImageProcessor.Playground/Program.cs @@ -50,75 +50,88 @@ namespace ImageProcessor.PlayGround // Image mask = Image.FromFile(Path.Combine(resolvedPath, "mask.png")); // Image overlay = Image.FromFile(Path.Combine(resolvedPath, "imageprocessor.128.png")); - FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "bob_revolutionpro_wilderness_02_2013_72dpi_2000x2000.jpg")); - IEnumerable files = GetFilesByExtensions(di, ".gif"); + FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "ej.jpg")); + //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "new-york.jpg")); + //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "mountain.jpg")); + //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "Arc-de-Triomphe-France.jpg")); + //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "Martin-Schoeller-Jack-Nicholson-Portrait.jpeg")); + //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "crop-base-300x200.jpg")); + //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "cmyk.png")); + //IEnumerable files = GetFilesByExtensions(di, ".jpg", ".jpeg", ".jfif"); + //IEnumerable files = GetFilesByExtensions(di, ".gif", ".webp", ".bmp", ".jpg", ".png", ".tif"); - // foreach (FileInfo fileInfo in files) - // { - byte[] photoBytes = File.ReadAllBytes(fileInfo.FullName); - Console.WriteLine("Processing: " + fileInfo.Name); + //foreach (FileInfo fileInfo in files) + //{ + //if (fileInfo.Name == "test5.jpg") + //{ + // continue; + //} - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); + byte[] photoBytes = File.ReadAllBytes(fileInfo.FullName); + Console.WriteLine("Processing: " + fileInfo.Name); - // ImageProcessor - using (MemoryStream inStream = new MemoryStream(photoBytes)) - { - using (ImageFactory imageFactory = new ImageFactory(true)) + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + // ImageProcessor + using (MemoryStream inStream = new MemoryStream(photoBytes)) { - Size size = new Size(844, 1017); - ResizeLayer layer = new ResizeLayer(size, ResizeMode.Max, AnchorPosition.Center, false); - - //ContentAwareResizeLayer layer = new ContentAwareResizeLayer(size) - //{ - // ConvolutionType = ConvolutionType.Sobel - //}; - // Load, resize, set the format and quality and save an image. - imageFactory.Load(inStream) - //.Overlay(new ImageLayer - // { - // Image = overlay, - // Opacity = 50 - // }) - //.Alpha(50) - //.BackgroundColor(Color.White) - //.Resize(new Size((int)(size.Width * 1.1), 0)) - //.ContentAwareResize(layer) - //.Constrain(size) - //.Mask(mask) - //.Format(new PngFormat()) - //.BackgroundColor(Color.Cyan) - //.ReplaceColor(Color.FromArgb(255, 223, 224), Color.FromArgb(121, 188, 255), 128) - //.Resize(size) - //.Resize(new ResizeLayer(size, ResizeMode.Max)) - // .Resize(new ResizeLayer(size, ResizeMode.Stretch)) - //.DetectEdges(new Laplacian3X3EdgeFilter(), true) - //.DetectEdges(new LaplacianOfGaussianEdgeFilter()) - .EntropyCrop() - //.Filter(MatrixFilters.Invert) - //.Contrast(50) - //.Filter(MatrixFilters.Comic) - //.Flip() - //.Filter(MatrixFilters.HiSatch) - //.Pixelate(8) - //.Rotate(45) - //.GaussianSharpen(10) - //.Format(new PngFormat() { IsIndexed = true }) - //.Format(new PngFormat() { IsIndexed = true }) - //.Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", fileInfo.Name))); - .Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", Path.GetFileNameWithoutExtension(fileInfo.Name) + ".png"))); - - stopwatch.Stop(); + using (ImageFactory imageFactory = new ImageFactory(true)) + { + Size size = new Size(844, 1017); + ResizeLayer layer = new ResizeLayer(size, ResizeMode.Max, AnchorPosition.Center, false); + + //ContentAwareResizeLayer layer = new ContentAwareResizeLayer(size) + //{ + // ConvolutionType = ConvolutionType.Sobel + //}; + // Load, resize, set the format and quality and save an image. + imageFactory.Load(inStream) + //.Overlay(new ImageLayer + // { + // Image = overlay, + // Opacity = 50 + // }) + //.Alpha(50) + //.BackgroundColor(Color.White) + //.Resize(new Size((int)(size.Width * 1.1), 0)) + //.ContentAwareResize(layer) + //.Constrain(size) + //.Mask(mask) + //.Format(new PngFormat()) + //.BackgroundColor(Color.Cyan) + //.ReplaceColor(Color.FromArgb(255, 223, 224), Color.FromArgb(121, 188, 255), 128) + //.Resize(size) + //.Resize(new ResizeLayer(size, ResizeMode.Max)) + // .Resize(new ResizeLayer(size, ResizeMode.Stretch)) + //.DetectEdges(new Laplacian3X3EdgeFilter(), true) + //.DetectEdges(new LaplacianOfGaussianEdgeFilter()) + //.EntropyCrop() + .Halftone() + //.Filter(MatrixFilters.Invert) + //.Contrast(50) + //.Filter(MatrixFilters.Comic) + //.Flip() + //.Filter(MatrixFilters.HiSatch) + //.Pixelate(8) + //.Rotate(45) + //.GaussianSharpen(10) + //.Format(new PngFormat() { IsIndexed = true }) + //.Format(new PngFormat() { IsIndexed = true }) + //.Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", fileInfo.Name))); + .Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", Path.GetFileNameWithoutExtension(fileInfo.Name) + ".png"))); + + stopwatch.Stop(); + } } - } - long peakWorkingSet64 = Process.GetCurrentProcess().PeakWorkingSet64; - float mB = peakWorkingSet64 / (float)1024 / 1024; + long peakWorkingSet64 = Process.GetCurrentProcess().PeakWorkingSet64; + float mB = peakWorkingSet64 / (float)1024 / 1024; - Console.WriteLine(@"Completed {0} in {1:s\.fff} secs {2}Peak memory usage was {3} bytes or {4} Mb.", fileInfo.Name, stopwatch.Elapsed, Environment.NewLine, peakWorkingSet64.ToString("#,#"), mB); + Console.WriteLine(@"Completed {0} in {1:s\.fff} secs {2}Peak memory usage was {3} bytes or {4} Mb.", fileInfo.Name, stopwatch.Elapsed, Environment.NewLine, peakWorkingSet64.ToString("#,#"), mB); - //Console.WriteLine("Processed: " + fileInfo.Name + " in " + stopwatch.ElapsedMilliseconds + "ms"); + //Console.WriteLine("Processed: " + fileInfo.Name + " in " + stopwatch.ElapsedMilliseconds + "ms"); //} Console.ReadLine(); diff --git a/src/ImageProcessor.Playground/images/input/Martin-Schoeller-Jack-Nicholson-Portrait.jpeg.REMOVED.git-id b/src/ImageProcessor.Playground/images/input/Martin-Schoeller-Jack-Nicholson-Portrait.jpeg.REMOVED.git-id new file mode 100644 index 0000000000..421c522013 --- /dev/null +++ b/src/ImageProcessor.Playground/images/input/Martin-Schoeller-Jack-Nicholson-Portrait.jpeg.REMOVED.git-id @@ -0,0 +1 @@ +ea3907f002c75115c976edb47c9a8ba28e76ad9a \ No newline at end of file diff --git a/src/ImageProcessor.Playground/images/input/cat.jpg.REMOVED.git-id b/src/ImageProcessor.Playground/images/input/cat.jpg.REMOVED.git-id new file mode 100644 index 0000000000..573b513a60 --- /dev/null +++ b/src/ImageProcessor.Playground/images/input/cat.jpg.REMOVED.git-id @@ -0,0 +1 @@ +c704af2c49aa45e35eab9e1b4839edbb7221cc7e \ No newline at end of file diff --git a/src/ImageProcessor.Playground/images/input/cmyk-test.png.REMOVED.git-id b/src/ImageProcessor.Playground/images/input/cmyk-test.png.REMOVED.git-id new file mode 100644 index 0000000000..0ab3955c16 --- /dev/null +++ b/src/ImageProcessor.Playground/images/input/cmyk-test.png.REMOVED.git-id @@ -0,0 +1 @@ +b6a165b747788ab89169b1b0ab7a66c1a67eeaee \ No newline at end of file diff --git a/src/ImageProcessor.Playground/images/input/cmyk.png b/src/ImageProcessor.Playground/images/input/cmyk.png new file mode 100644 index 0000000000..380f5e3866 --- /dev/null +++ b/src/ImageProcessor.Playground/images/input/cmyk.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e17ba8fac1fd6247ff839ab7e21d715c96b3588d5f4fdb835dbe8c937dbf79d +size 37678 diff --git a/src/ImageProcessor.Playground/images/input/mountain.jpg.REMOVED.git-id b/src/ImageProcessor.Playground/images/input/mountain.jpg.REMOVED.git-id index d2d29b9de2..5d799dafbc 100644 --- a/src/ImageProcessor.Playground/images/input/mountain.jpg.REMOVED.git-id +++ b/src/ImageProcessor.Playground/images/input/mountain.jpg.REMOVED.git-id @@ -1 +1 @@ -66b1f9f5ef7ca628ca4a44de9188e11e4af17229 \ No newline at end of file +961e0a9abe5cb414d6a34f6c40f713d88a826910 \ No newline at end of file diff --git a/src/ImageProcessor.UnitTests/Imaging/ColorUnitTests.cs b/src/ImageProcessor.UnitTests/Imaging/ColorUnitTests.cs index 76f9fd68f7..04b5e3d123 100644 --- a/src/ImageProcessor.UnitTests/Imaging/ColorUnitTests.cs +++ b/src/ImageProcessor.UnitTests/Imaging/ColorUnitTests.cs @@ -10,6 +10,7 @@ namespace ImageProcessor.UnitTests.Imaging { + using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Drawing; @@ -61,6 +62,18 @@ namespace ImageProcessor.UnitTests.Imaging first.Equals(second).Should().BeTrue("because the color structure should implement Equals()"); } + /// + /// Tests the struct equality operators. + /// + [Test] + public void CmykColorImplementsEquals() + { + CmykColor first = CmykColor.FromColor(Color.White); + CmykColor second = CmykColor.FromColor(Color.White); + + first.Equals(second).Should().BeTrue("because the color structure should implement Equals()"); + } + /// /// Test conversion to and from a . /// @@ -91,6 +104,40 @@ namespace ImageProcessor.UnitTests.Imaging result.Should().Be(expected); } + /// + /// Test conversion to and from a . + /// + /// + /// The expected output. + /// + [Test] + [TestCase("#FFFFFF")] + [TestCase("#FEFFFE")] + [TestCase("#F0F8FF")] + [TestCase("#000000")] + [TestCase("#CCFF33")] + [TestCase("#00FF00")] + [TestCase("#FF00FF")] + [TestCase("#990000")] + [TestCase("#5C955C")] + [TestCase("#5C5C95")] + [TestCase("#3F3F66")] + [TestCase("#FFFFBB")] + [TestCase("#FF002B")] + [TestCase("#00ABFF")] + public void CmykColorShouldConvertToAndFromString(string expected) + { + Color color = ColorTranslator.FromHtml(expected); + CmykColor cmykColor = CmykColor.FromColor(color); + + Debug.Print(cmykColor.ToString()); + + + string result = ColorTranslator.ToHtml(cmykColor); + + result.Should().Be(expected); + } + /// /// Test conversion to and from a . /// @@ -122,7 +169,7 @@ namespace ImageProcessor.UnitTests.Imaging } /// - /// Test conversion to and from a . + /// Test conversion to and from a . /// /// /// The expected output. diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs index 1077b2fb69..0d07129e38 100644 --- a/src/ImageProcessor/ImageFactory.cs +++ b/src/ImageProcessor/ImageFactory.cs @@ -681,6 +681,17 @@ namespace ImageProcessor return this; } + public ImageFactory Halftone() + { + if (this.ShouldProcess) + { + Halftone halftone = new Halftone(); + this.CurrentImageFormat.ApplyProcessor(halftone.ProcessImage, this); + } + + return this; + } + /// /// Applies the given image mask to the current image. /// diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index 0ad2b8a89d..88d010e60e 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -131,13 +131,16 @@ + Code + + @@ -223,6 +226,7 @@ + diff --git a/src/ImageProcessor/Imaging/Colors/CmykColor.cs b/src/ImageProcessor/Imaging/Colors/CmykColor.cs new file mode 100644 index 0000000000..9ec02cd414 --- /dev/null +++ b/src/ImageProcessor/Imaging/Colors/CmykColor.cs @@ -0,0 +1,383 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Represents an CMYK (cyan, magenta, yellow, keyline) color. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Imaging.Colors +{ + using System; + using System.Drawing; + + using ImageProcessor.Common.Extensions; + + /// + /// Represents an CMYK (cyan, magenta, yellow, keyline) color. + /// + public struct CmykColor + { + /// + /// Represents a that is null. + /// + public static readonly CmykColor Empty = new CmykColor(); + + /// + /// The cyan color component. + /// + private readonly float c; + + /// + /// The magenta color component. + /// + private readonly float m; + + /// + /// The yellow color component. + /// + private readonly float y; + + /// + /// The keyline black color component. + /// + private readonly float k; + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The cyan component. + /// + /// + /// The magenta component. + /// + /// + /// The yellow component. + /// + /// + /// The keyline black component. + /// + private CmykColor(float cyan, float magenta, float yellow, float keyline) + { + this.c = Clamp(cyan); + this.m = Clamp(magenta); + this.y = Clamp(yellow); + this.k = Clamp(keyline); + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The to initialize from. + /// + private CmykColor(Color color) + { + CmykColor cmykColor = color; + this.c = cmykColor.c; + this.m = cmykColor.m; + this.y = cmykColor.y; + this.k = cmykColor.k; + } + + /// + /// Gets the cyan component. + /// A value ranging between 0 and 100. + /// + public float C + { + get + { + return this.c; + } + } + + /// + /// Gets the magenta component. + /// A value ranging between 0 and 100. + /// + public float M + { + get + { + return this.m; + } + } + + /// + /// Gets the yellow component. + /// A value ranging between 0 and 100. + /// + public float Y + { + get + { + return this.y; + } + } + + /// + /// Gets the keyline black component. + /// A value ranging between 0 and 100. + /// + public float K + { + get + { + return this.k; + } + } + + /// + /// Creates a structure from the four 32-bit CMYK + /// components (cyan, magenta, yellow, and keyline) values. + /// + /// + /// The cyan component. + /// + /// + /// The magenta component. + /// + /// + /// The yellow component. + /// + /// + /// The keyline black component. + /// + /// + /// The . + /// + public static CmykColor FromCmykColor(float cyan, float magenta, float yellow, float keyline) + { + return new CmykColor(cyan, magenta, yellow, keyline); + } + + /// + /// Creates a structure from the specified structure + /// + /// + /// The from which to create the new . + /// + /// + /// The . + /// + public static CmykColor FromColor(Color color) + { + return new CmykColor(color); + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator CmykColor(Color color) + { + float c = (255f - color.R) / 255; + float m = (255f - color.G) / 255; + float y = (255f - color.B) / 255; + + float k = Math.Min(c, Math.Min(m, y)); + + if (Math.Abs(k - 1.0) <= .0001f) + { + return new CmykColor(0, 0, 0, 100); + } + + c = ((c - k) / (1 - k)) * 100; + m = ((m - k) / (1 - k)) * 100; + y = ((y - k) / (1 - k)) * 100; + + return new CmykColor(c, m, y, k * 100); + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator CmykColor(RgbaColor rgbaColor) + { + return FromColor(rgbaColor); + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator CmykColor(YCbCrColor ycbcrColor) + { + Color color = ycbcrColor; + return FromColor(color); + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator Color(CmykColor cmykColor) + { + int red = Convert.ToInt32((1 - (cmykColor.c / 100)) * (1 - (cmykColor.k / 100)) * 255.0); + int green = Convert.ToInt32((1 - (cmykColor.m / 100)) * (1 - (cmykColor.k / 100)) * 255.0); + int blue = Convert.ToInt32((1 - (cmykColor.y / 100)) * (1 - (cmykColor.k / 100)) * 255.0); + return Color.FromArgb(red.ToByte(), green.ToByte(), blue.ToByte()); + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator RgbaColor(CmykColor cmykColor) + { + int red = Convert.ToInt32((1 - (cmykColor.c / 100)) * (1 - (cmykColor.k / 100)) * 255.0); + int green = Convert.ToInt32((1 - (cmykColor.m / 100)) * (1 - (cmykColor.k / 100)) * 255.0); + int blue = Convert.ToInt32((1 - (cmykColor.y / 100)) * (1 - (cmykColor.k / 100)) * 255.0); + return RgbaColor.FromRgba(red.ToByte(), green.ToByte(), blue.ToByte()); + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator YCbCrColor(CmykColor cmykColor) + { + return YCbCrColor.FromColor(cmykColor); + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator HslaColor(CmykColor cmykColor) + { + return HslaColor.FromColor(cmykColor); + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + if (this.IsEmpty()) + { + return "CmykColor [Empty]"; + } + + return string.Format("CmykColor [ C={0:#0.##}, M={1:#0.##}, Y={2:#0.##}, K={3:#0.##}]", this.C, this.M, this.Y, this.K); + } + + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// + /// true if and this instance are the same type and represent the same value; otherwise, false. + /// + /// Another object to compare to. + public override bool Equals(object obj) + { + if (obj is CmykColor) + { + Color thisColor = this; + Color otherColor = (CmykColor)obj; + + return thisColor.Equals(otherColor); + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + public override int GetHashCode() + { + Color thisColor = this; + return thisColor.GetHashCode(); + } + + /// + /// Checks the range of the given value to ensure that it remains within the acceptable boundaries. + /// + /// + /// The value to check. + /// + /// + /// The sanitized . + /// + private static float Clamp(float value) + { + if (value < 0.0) + { + value = 0.0f; + } + else if (value > 100) + { + value = 100f; + } + + return value; + } + + /// + /// Returns a value indicating whether the current instance is empty. + /// + /// + /// The true if this instance is empty; otherwise, false. + /// + private bool IsEmpty() + { + const float Epsilon = .0001f; + return Math.Abs(this.c - 0) <= Epsilon && Math.Abs(this.m - 0) <= Epsilon && + Math.Abs(this.y - 0) <= Epsilon && Math.Abs(this.k - 0) <= Epsilon; + } + } +} diff --git a/src/ImageProcessor/Imaging/Colors/ColorExtensions.cs b/src/ImageProcessor/Imaging/Colors/ColorExtensions.cs new file mode 100644 index 0000000000..f0f36efa06 --- /dev/null +++ b/src/ImageProcessor/Imaging/Colors/ColorExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ImageProcessor.Imaging.Colors +{ + using System.Drawing; + + using ImageProcessor.Common.Extensions; + + internal static class ColorExtensions + { + public static Color Add(this Color color, params Color[] colors) + { + int red = color.A > 0 ? color.R : 0; + int green = color.A > 0 ? color.G : 0; + int blue = color.A > 0 ? color.B : 0; + int alpha = color.A; + + int counter = 0; + foreach (Color addColor in colors) + { + if (addColor.A > 0) + { + counter += 1; + red += addColor.R; + green += addColor.G; + blue += addColor.B; + alpha += addColor.A; + } + } + + counter = Math.Max(1, counter); + + return Color.FromArgb((alpha / counter).ToByte(), (red / counter).ToByte(), (green / counter).ToByte(), (blue / counter).ToByte()); + } + + public static CmykColor AddAsCmykColor(this Color color, params Color[] colors) + { + CmykColor cmyk = color; + float c = color.A > 0 ? cmyk.C : 0; + float m = color.A > 0 ? cmyk.M : 0; + float y = color.A > 0 ? cmyk.Y : 0; + float k = color.A > 0 ? cmyk.K : 0; + + foreach (Color addColor in colors) + { + if (addColor.A > 0) + { + CmykColor cmykAdd = addColor; + c += cmykAdd.C; + m += cmykAdd.M; + y += cmykAdd.Y; + k += cmykAdd.K; + } + } + + //c = Math.Max(0.0f, Math.Min(100f, c)); + //m = Math.Max(0.0f, Math.Min(100f, m)); + //y = Math.Max(0.0f, Math.Min(100f, y)); + //k = Math.Max(0.0f, Math.Min(100f, k)); + + return CmykColor.FromCmykColor(c, m, y, k); + } + } +} diff --git a/src/ImageProcessor/Imaging/Colors/HSLAColor.cs b/src/ImageProcessor/Imaging/Colors/HSLAColor.cs index 24939766bf..079b54b846 100644 --- a/src/ImageProcessor/Imaging/Colors/HSLAColor.cs +++ b/src/ImageProcessor/Imaging/Colors/HSLAColor.cs @@ -336,6 +336,21 @@ namespace ImageProcessor.Imaging.Colors return YCbCrColor.FromColor(hslaColor); } + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator CmykColor(HslaColor hslaColor) + { + return CmykColor.FromColor(hslaColor); + } + /// /// Returns a that represents this instance. /// @@ -346,10 +361,10 @@ namespace ImageProcessor.Imaging.Colors { if (this.IsEmpty()) { - return "HSLAColor [Empty]"; + return "HslaColor [Empty]"; } - return string.Format("HSLAColor [ H={0:#0.##}, S={1:#0.##}, L={2:#0.##}, A={3:#0.##}]", this.H, this.S, this.L, this.A); + return string.Format("HslaColor [ H={0:#0.##}, S={1:#0.##}, L={2:#0.##}, A={3:#0.##}]", this.H, this.S, this.L, this.A); } /// diff --git a/src/ImageProcessor/Imaging/Colors/RGBAColor.cs b/src/ImageProcessor/Imaging/Colors/RGBAColor.cs index 790e2ae09b..e8b3bbb66c 100644 --- a/src/ImageProcessor/Imaging/Colors/RGBAColor.cs +++ b/src/ImageProcessor/Imaging/Colors/RGBAColor.cs @@ -228,45 +228,60 @@ namespace ImageProcessor.Imaging.Colors /// Allows the implicit conversion of an instance of to a /// . /// - /// + /// /// The instance of to convert. /// /// /// An instance of . /// - public static implicit operator Color(RgbaColor rgba) + public static implicit operator Color(RgbaColor rgbaColor) { - return Color.FromArgb(rgba.A, rgba.R, rgba.G, rgba.B); + return Color.FromArgb(rgbaColor.A, rgbaColor.R, rgbaColor.G, rgbaColor.B); } /// /// Allows the implicit conversion of an instance of to a /// . /// - /// + /// /// The instance of to convert. /// /// /// An instance of . /// - public static implicit operator HslaColor(RgbaColor rgba) + public static implicit operator HslaColor(RgbaColor rgbaColor) { - return HslaColor.FromColor(rgba); + return HslaColor.FromColor(rgbaColor); } /// /// Allows the implicit conversion of an instance of to a /// . /// - /// + /// /// The instance of to convert. /// /// /// An instance of . /// - public static implicit operator YCbCrColor(RgbaColor rgba) + public static implicit operator YCbCrColor(RgbaColor rgbaColor) { - return YCbCrColor.FromColor(rgba); + return YCbCrColor.FromColor(rgbaColor); + } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator CmykColor(RgbaColor rgbaColor) + { + return CmykColor.FromColor(rgbaColor); } /// diff --git a/src/ImageProcessor/Imaging/Colors/YCbCrColor.cs b/src/ImageProcessor/Imaging/Colors/YCbCrColor.cs index 8f1c001c81..e8f881f09e 100644 --- a/src/ImageProcessor/Imaging/Colors/YCbCrColor.cs +++ b/src/ImageProcessor/Imaging/Colors/YCbCrColor.cs @@ -224,6 +224,22 @@ namespace ImageProcessor.Imaging.Colors return HslaColor.FromColor(ycbcrColor); } + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator CmykColor(YCbCrColor ycbcrColor) + { + return CmykColor.FromColor(ycbcrColor); + } + /// /// Returns a that represents this instance. /// diff --git a/src/ImageProcessor/Imaging/FastBitmap.cs b/src/ImageProcessor/Imaging/FastBitmap.cs index 5b6fcb8182..f8b14deb20 100644 --- a/src/ImageProcessor/Imaging/FastBitmap.cs +++ b/src/ImageProcessor/Imaging/FastBitmap.cs @@ -288,7 +288,7 @@ namespace ImageProcessor.Imaging } // Lock the bitmap - this.bitmapData = this.bitmap.LockBits(bounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); + this.bitmapData = this.bitmap.LockBits(bounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb); // Set the value to the first scan line this.pixelBase = (byte*)this.bitmapData.Scan0.ToPointer(); diff --git a/src/ImageProcessor/Imaging/Filters/Artistic/HalftoneFilter.cs b/src/ImageProcessor/Imaging/Filters/Artistic/HalftoneFilter.cs new file mode 100644 index 0000000000..3b4f93f035 --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/Artistic/HalftoneFilter.cs @@ -0,0 +1,346 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// The halftone filter applies a classical CMYK filter to the given image. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Imaging.Filters.Artistic +{ + using System; + using System.Drawing; + using System.Threading.Tasks; + + using ImageProcessor.Imaging.Colors; + using ImageProcessor.Imaging.Helpers; + + /// + /// The halftone filter applies a classical CMYK filter to the given image. + /// + public class HalftoneFilter + { + /// + /// The angle of the cyan component in degrees. + /// + private float cyanAngle = 15f; + + /// + /// The angle of the magenta component in degrees. + /// + private float magentaAngle = 75f; + + /// + /// The angle of the yellow component in degrees. + /// + private float yellowAngle = 0f; + + /// + /// The angle of the keyline component in degrees. + /// + private float keylineAngle = 45f; + + /// + /// The distance between component points. + /// + private int distance = 4; + + /// + /// Initializes a new instance of the class. + /// + public HalftoneFilter() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The distance. + /// + public HalftoneFilter(int distance) + { + this.distance = distance; + } + + /// + /// Gets or sets the angle of the cyan component in degrees. + /// + public float CyanAngle + { + get + { + return this.cyanAngle; + } + + set + { + this.cyanAngle = value; + } + } + + /// + /// Gets or sets the angle of the magenta component in degrees. + /// + public float MagentaAngle + { + get + { + return this.magentaAngle; + } + + set + { + this.magentaAngle = value; + } + } + + /// + /// Gets or sets the angle of the yellow component in degrees. + /// + public float YellowAngle + { + get + { + return this.yellowAngle; + } + + set + { + this.yellowAngle = value; + } + } + + /// + /// Gets or sets the angle of the keyline black component in degrees. + /// + public float KeylineAngle + { + get + { + return this.keylineAngle; + } + + set + { + this.keylineAngle = value; + } + } + + /// + /// Gets or sets the distance between component points. + /// + public int Distance + { + get + { + return this.distance; + } + + set + { + this.distance = value; + } + } + + /// + /// Applies the filter. TODO: Make this class implement an interface? + /// + /// + /// The to apply the filter to. + /// + /// + /// The with the filter applied. + /// + public Bitmap ApplyFilter(Bitmap source) + { + Bitmap cyan = null; + Bitmap magenta = null; + Bitmap yellow = null; + Bitmap keyline = null; + Bitmap newImage = null; + + try + { + int width = source.Width; + int height = source.Height; + + float multiplier = 4 * (float)Math.Sqrt(2); + float max = this.distance + (float)Math.Sqrt(2); + float keylineMax = max + 1; + + // Cyan color sampled from Wikipedia page. Keyline brush is declared + // separately. + Brush cyanBrush = new SolidBrush(Color.FromArgb(0, 153, 239)); + Brush magentaBrush = Brushes.Magenta; + Brush yellowBrush = Brushes.Yellow; + + // Create our images. + cyan = new Bitmap(width, height); + magenta = new Bitmap(width, height); + yellow = new Bitmap(width, height); + keyline = new Bitmap(width, height); + newImage = new Bitmap(width, height); + + // Ensure the correct resolution is set. + cyan.SetResolution(source.HorizontalResolution, source.VerticalResolution); + magenta.SetResolution(source.HorizontalResolution, source.VerticalResolution); + yellow.SetResolution(source.HorizontalResolution, source.VerticalResolution); + keyline.SetResolution(source.HorizontalResolution, source.VerticalResolution); + newImage.SetResolution(source.HorizontalResolution, source.VerticalResolution); + + // Check bounds against this. + Rectangle rectangle = new Rectangle(0, 0, width, height); + + using (Graphics graphicsCyan = Graphics.FromImage(cyan)) + using (Graphics graphicsMagenta = Graphics.FromImage(magenta)) + using (Graphics graphicsYellow = Graphics.FromImage(yellow)) + using (Graphics graphicsKeyline = Graphics.FromImage(keyline)) + { + // Ensure cleared out. + graphicsCyan.Clear(Color.Transparent); + graphicsMagenta.Clear(Color.Transparent); + graphicsYellow.Clear(Color.Transparent); + graphicsKeyline.Clear(Color.Transparent); + + // This is too slow. The graphics object can't be called within a parallel + // loop so we have to do it old school. :( + using (FastBitmap sourceBitmap = new FastBitmap(source)) + { + for (int y = -height * 2; y < height * 2; y += this.distance) + { + for (int x = -width * 2; x < width * 2; x += this.distance) + { + Color color; + CmykColor cmykColor; + float brushWidth; + + // Cyan + Point rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), this.cyanAngle); + int angledX = rotatedPoint.X; + int angledY = rotatedPoint.Y; + if (rectangle.Contains(new Point(angledX, angledY))) + { + color = sourceBitmap.GetPixel(angledX, angledY); + cmykColor = color; + brushWidth = Math.Max(0, Math.Min(max, this.distance * (cmykColor.C / 255f) * multiplier)); + graphicsCyan.FillEllipse(cyanBrush, angledX, angledY, brushWidth, brushWidth); + } + + // Magenta + rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), this.magentaAngle); + angledX = rotatedPoint.X; + angledY = rotatedPoint.Y; + if (rectangle.Contains(new Point(angledX, angledY))) + { + color = sourceBitmap.GetPixel(angledX, angledY); + cmykColor = color; + brushWidth = Math.Max(0, Math.Min(max, this.distance * (cmykColor.M / 255f) * multiplier)); + graphicsMagenta.FillEllipse(magentaBrush, angledX, angledY, brushWidth, brushWidth); + } + + // Yellow + rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), this.yellowAngle); + angledX = rotatedPoint.X; + angledY = rotatedPoint.Y; + if (rectangle.Contains(new Point(angledX, angledY))) + { + color = sourceBitmap.GetPixel(angledX, angledY); + cmykColor = color; + brushWidth = Math.Max(0, Math.Min(max, this.distance * (cmykColor.Y / 255f) * multiplier)); + graphicsYellow.FillEllipse(yellowBrush, angledX, angledY, brushWidth, brushWidth); + } + + // Keyline + rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), this.keylineAngle); + angledX = rotatedPoint.X; + angledY = rotatedPoint.Y; + if (rectangle.Contains(new Point(angledX, angledY))) + { + color = sourceBitmap.GetPixel(angledX, angledY); + cmykColor = color; + brushWidth = Math.Max(0, Math.Min(keylineMax, this.distance * (cmykColor.K / 255f) * multiplier)); + + // Just using black is too dark. + Brush keylineBrush = new SolidBrush(CmykColor.FromCmykColor(0, 0, 0, cmykColor.K)); + graphicsKeyline.FillEllipse(keylineBrush, angledX, angledY, brushWidth, brushWidth); + } + } + } + } + + // Set our white background. + using (Graphics graphics = Graphics.FromImage(newImage)) + { + graphics.Clear(Color.White); + } + + // Blend the colors now to mimic adaptive blending. + using (FastBitmap cyanBitmap = new FastBitmap(cyan)) + using (FastBitmap magentaBitmap = new FastBitmap(magenta)) + using (FastBitmap yellowBitmap = new FastBitmap(yellow)) + using (FastBitmap keylineBitmap = new FastBitmap(keyline)) + using (FastBitmap destinationBitmap = new FastBitmap(newImage)) + { + Parallel.For( + 0, + height, + y => + { + for (int x = 0; x < width; x++) + { + // ReSharper disable AccessToDisposedClosure + Color cyanPixel = cyanBitmap.GetPixel(x, y); + Color magentaPixel = magentaBitmap.GetPixel(x, y); + Color yellowPixel = yellowBitmap.GetPixel(x, y); + Color keylinePixel = keylineBitmap.GetPixel(x, y); + + CmykColor blended = cyanPixel.AddAsCmykColor(magentaPixel, yellowPixel, keylinePixel); + destinationBitmap.SetPixel(x, y, blended); + // ReSharper restore AccessToDisposedClosure + } + }); + } + } + + cyan.Dispose(); + magenta.Dispose(); + yellow.Dispose(); + keyline.Dispose(); + source.Dispose(); + source = newImage; + } + catch + { + if (cyan != null) + { + cyan.Dispose(); + } + + if (magenta != null) + { + magenta.Dispose(); + } + + if (yellow != null) + { + yellow.Dispose(); + } + + if (keyline != null) + { + keyline.Dispose(); + } + + if (newImage != null) + { + newImage.Dispose(); + } + } + + return source; + } + } +} diff --git a/src/ImageProcessor/Imaging/Helpers/Effects.cs b/src/ImageProcessor/Imaging/Helpers/Effects.cs index b2ce5c7403..a8d9821fdb 100644 --- a/src/ImageProcessor/Imaging/Helpers/Effects.cs +++ b/src/ImageProcessor/Imaging/Helpers/Effects.cs @@ -43,7 +43,7 @@ namespace ImageProcessor.Imaging.Helpers { using (Graphics graphics = Graphics.FromImage(source)) { - Rectangle bounds = rectangle.HasValue ? rectangle.Value : new Rectangle(0, 0, source.Width, source.Height); + Rectangle bounds = rectangle ?? new Rectangle(0, 0, source.Width, source.Height); Rectangle ellipsebounds = bounds; // Increase the rectangle size by the difference between the rectangle dimensions and sqrt(2)/2 * the rectangle dimensions. diff --git a/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs b/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs index a9e997d178..266082718a 100644 --- a/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs +++ b/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs @@ -151,7 +151,7 @@ namespace ImageProcessor.Imaging.Helpers bottomRight.X = getMaxX(fastBitmap) + 1; } - return ImageMaths.GetBoundingRectangle(topLeft, bottomRight); + return GetBoundingRectangle(topLeft, bottomRight); } /// @@ -194,5 +194,49 @@ namespace ImageProcessor.Imaging.Helpers new Point(rectangle.Left, rectangle.Bottom) }; } + + /// + /// Returns the given degrees converted to radians. + /// + /// + /// The angle in degrees. + /// + /// + /// The representing the degree as radians. + /// + public static double DegreesToRadians(double angleInDegrees) + { + return angleInDegrees * (Math.PI / 180); + } + + /// + /// Rotates one point around another + /// + /// + /// The point to rotate. + /// The rotation angle in degrees. + /// The centre point of rotation. If not set the point will equal + /// + /// + /// Rotated point + public static Point RotatePoint(Point pointToRotate, double angleInDegrees, Point? centerPoint = null) + { + Point center = centerPoint ?? Point.Empty; + + double angleInRadians = DegreesToRadians(angleInDegrees); + double cosTheta = Math.Cos(angleInRadians); + double sinTheta = Math.Sin(angleInRadians); + return new Point + { + X = + (int) + ((cosTheta * (pointToRotate.X - center.X)) - + ((sinTheta * (pointToRotate.Y - center.Y)) + center.X)), + Y = + (int) + ((sinTheta * (pointToRotate.X - center.X)) + + ((cosTheta * (pointToRotate.Y - center.Y)) + center.Y)) + }; + } } } diff --git a/src/ImageProcessor/Imaging/Quantizers/Quantizer.cs b/src/ImageProcessor/Imaging/Quantizers/Quantizer.cs index 1a269da3ff..f1eaae6630 100644 --- a/src/ImageProcessor/Imaging/Quantizers/Quantizer.cs +++ b/src/ImageProcessor/Imaging/Quantizers/Quantizer.cs @@ -62,7 +62,7 @@ namespace ImageProcessor.Imaging.Quantizers Rectangle bounds = new Rectangle(0, 0, width, height); // First off take a 32bpp copy of the image - Bitmap copy = new Bitmap(width, height, PixelFormat.Format32bppArgb); + Bitmap copy = new Bitmap(width, height, PixelFormat.Format32bppPArgb); copy.SetResolution(source.HorizontalResolution, source.VerticalResolution); // And construct an 8bpp version @@ -85,7 +85,7 @@ namespace ImageProcessor.Imaging.Quantizers try { // Get the source image bits and lock into memory - sourceData = copy.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + sourceData = copy.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppPArgb); // Call the FirstPass function if not a single pass algorithm. // For something like an Octree quantizer, this will run through diff --git a/src/ImageProcessor/Processors/Halftone - Copy.cs b/src/ImageProcessor/Processors/Halftone - Copy.cs new file mode 100644 index 0000000000..e43ce19dee --- /dev/null +++ b/src/ImageProcessor/Processors/Halftone - Copy.cs @@ -0,0 +1,262 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Processors +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Drawing; + using System.Drawing.Drawing2D; + using System.Threading.Tasks; + + using ImageProcessor.Common.Exceptions; + using ImageProcessor.Common.Extensions; + using ImageProcessor.Imaging; + using ImageProcessor.Imaging.Colors; + using ImageProcessor.Imaging.Helpers; + + /// + /// The halftone. + /// + class Halftone : IGraphicsProcessor + { + /// + /// Initializes a new instance of the class. + /// + public Halftone() + { + this.Settings = new Dictionary(); + } + + /// + /// Gets or sets the dynamic parameter. + /// + public dynamic DynamicParameter + { + get; + set; + } + + /// + /// Gets or sets any additional settings required by the processor. + /// + public Dictionary Settings + { + get; + set; + } + + /// + /// The process image. + /// + /// + /// The factory. + /// + /// + /// The . + /// + /// + /// + public Image ProcessImage(ImageFactory factory) + { + Bitmap cyan = null; + Bitmap magenta = null; + Bitmap yellow = null; + Bitmap keyline = null; + Bitmap newImage = null; + Image image = factory.Image; + + try + { + int width = image.Width; + int height = image.Height; + + // Angles taken from Wikipedia page. + float cyanAngle = 15f; + float magentaAngle = 75f; + float yellowAngle = 0f; + float keylineAngle = 45f; + + int diameter = 4; + float multiplier = 4 * (float)Math.Sqrt(2); + + // Cyan color sampled from Wikipedia page. + Brush cyanBrush = new SolidBrush(Color.FromArgb(0, 153, 239)); + Brush magentaBrush = Brushes.Magenta; + Brush yellowBrush = Brushes.Yellow; + Brush keylineBrush; + + // Create our images. + cyan = new Bitmap(width, height); + magenta = new Bitmap(width, height); + yellow = new Bitmap(width, height); + keyline = new Bitmap(width, height); + newImage = new Bitmap(width, height); + + // Ensure the correct resolution is set. + cyan.SetResolution(image.HorizontalResolution, image.VerticalResolution); + magenta.SetResolution(image.HorizontalResolution, image.VerticalResolution); + yellow.SetResolution(image.HorizontalResolution, image.VerticalResolution); + keyline.SetResolution(image.HorizontalResolution, image.VerticalResolution); + newImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); + + // Check bounds against this. + Rectangle rectangle = new Rectangle(0, 0, width, height); + + using (Graphics graphicsCyan = Graphics.FromImage(cyan)) + using (Graphics graphicsMagenta = Graphics.FromImage(magenta)) + using (Graphics graphicsYellow = Graphics.FromImage(yellow)) + using (Graphics graphicsKeyline = Graphics.FromImage(keyline)) + { + // Ensure cleared out. + graphicsCyan.Clear(Color.Transparent); + graphicsMagenta.Clear(Color.Transparent); + graphicsYellow.Clear(Color.Transparent); + graphicsKeyline.Clear(Color.Transparent); + + // This is too slow. The graphics object can't be called within a parallel + // loop so we have to do it old school. :( + using (FastBitmap sourceBitmap = new FastBitmap(image)) + { + for (int y = -height * 2; y < height * 2; y += diameter) + { + for (int x = -width * 2; x < width * 2; x += diameter) + { + Color color; + CmykColor cmykColor; + float brushWidth; + + // Cyan + Point rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), cyanAngle); + int angledX = rotatedPoint.X; + int angledY = rotatedPoint.Y; + if (rectangle.Contains(new Point(angledX, angledY))) + { + color = sourceBitmap.GetPixel(angledX, angledY); + cmykColor = color; + brushWidth = diameter * (cmykColor.C / 255f) * multiplier; + graphicsCyan.FillEllipse(cyanBrush, angledX, angledY, brushWidth, brushWidth); + } + + // Magenta + rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), magentaAngle); + angledX = rotatedPoint.X; + angledY = rotatedPoint.Y; + if (rectangle.Contains(new Point(angledX, angledY))) + { + color = sourceBitmap.GetPixel(angledX, angledY); + cmykColor = color; + brushWidth = diameter * (cmykColor.M / 255f) * multiplier; + graphicsMagenta.FillEllipse(magentaBrush, angledX, angledY, brushWidth, brushWidth); + } + + // Yellow + rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), yellowAngle); + angledX = rotatedPoint.X; + angledY = rotatedPoint.Y; + if (rectangle.Contains(new Point(angledX, angledY))) + { + color = sourceBitmap.GetPixel(angledX, angledY); + cmykColor = color; + brushWidth = diameter * (cmykColor.Y / 255f) * multiplier; + graphicsYellow.FillEllipse(yellowBrush, angledX, angledY, brushWidth, brushWidth); + } + + // Keyline + rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), keylineAngle); + angledX = rotatedPoint.X; + angledY = rotatedPoint.Y; + if (rectangle.Contains(new Point(angledX, angledY))) + { + color = sourceBitmap.GetPixel(angledX, angledY); + cmykColor = color; + brushWidth = diameter * (cmykColor.K / 255f) * multiplier; + + // Just using blck is too dark. + keylineBrush = new SolidBrush(CmykColor.FromCmykColor(0, 0, 0, cmykColor.K)); + graphicsKeyline.FillEllipse(keylineBrush, angledX, angledY, brushWidth, brushWidth); + } + } + } + } + + // Set our white background. + using (Graphics graphics = Graphics.FromImage(newImage)) + { + graphics.Clear(Color.White); + } + + // Blend the colors now to mimic adaptive blending. + using (FastBitmap cyanBitmap = new FastBitmap(cyan)) + using (FastBitmap magentaBitmap = new FastBitmap(magenta)) + using (FastBitmap yellowBitmap = new FastBitmap(yellow)) + using (FastBitmap keylineBitmap = new FastBitmap(keyline)) + using (FastBitmap destinationBitmap = new FastBitmap(newImage)) + { + Parallel.For( + 0, + height, + y => + { + for (int x = 0; x < width; x++) + { + // ReSharper disable AccessToDisposedClosure + Color cyanPixel = cyanBitmap.GetPixel(x, y); + Color magentaPixel = magentaBitmap.GetPixel(x, y); + Color yellowPixel = yellowBitmap.GetPixel(x, y); + Color keylinePixel = keylineBitmap.GetPixel(x, y); + + CmykColor blended = cyanPixel.AddAsCmykColor(magentaPixel, yellowPixel, keylinePixel); + destinationBitmap.SetPixel(x, y, blended); + // ReSharper restore AccessToDisposedClosure + } + }); + } + } + + cyan.Dispose(); + magenta.Dispose(); + yellow.Dispose(); + keyline.Dispose(); + image.Dispose(); + image = newImage; + } + catch (Exception ex) + { + if (cyan != null) + { + cyan.Dispose(); + } + + if (magenta != null) + { + magenta.Dispose(); + } + + if (yellow != null) + { + yellow.Dispose(); + } + + if (keyline != null) + { + keyline.Dispose(); + } + + if (newImage != null) + { + newImage.Dispose(); + } + + throw new ImageProcessingException("Error processing image with " + this.GetType().Name, ex); + } + + return image; + } + } +} diff --git a/src/ImageProcessor/Processors/Halftone.cs b/src/ImageProcessor/Processors/Halftone.cs new file mode 100644 index 0000000000..d1b149b687 --- /dev/null +++ b/src/ImageProcessor/Processors/Halftone.cs @@ -0,0 +1,181 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// The halftone processor applies a classical CMYK halftone to the given image. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Processors +{ + using System; + using System.Collections.Generic; + using System.Drawing; + using System.Drawing.Imaging; + using System.Threading.Tasks; + + using ImageProcessor.Common.Exceptions; + using ImageProcessor.Imaging; + using ImageProcessor.Imaging.Filters.Artistic; + using ImageProcessor.Imaging.Filters.EdgeDetection; + using ImageProcessor.Imaging.Filters.Photo; + using ImageProcessor.Imaging.Helpers; + + /// + /// The halftone processor applies a classical CMYK halftone to the given image. + /// + public class Halftone : IGraphicsProcessor + { + /// + /// Initializes a new instance of the class. + /// + public Halftone() + { + this.Settings = new Dictionary(); + } + + /// + /// Gets or sets the dynamic parameter. + /// + public dynamic DynamicParameter + { + get; + set; + } + + /// + /// Gets or sets any additional settings required by the processor. + /// + public Dictionary Settings + { + get; + set; + } + + /// + /// Processes the image. + /// + /// + /// The current instance of the class containing + /// the image to process. + /// + /// + /// The processed image from the current instance of the class. + /// + public Image ProcessImage(ImageFactory factory) + { + Image image = factory.Image; + int width = image.Width; + int height = image.Height; + Bitmap newImage = null; + Bitmap edgeBitmap = null; + try + { + HalftoneFilter filter = new HalftoneFilter(5); + newImage = new Bitmap(image); + newImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); + newImage = filter.ApplyFilter(newImage); + + // Draw the edges. + edgeBitmap = new Bitmap(width, height); + edgeBitmap.SetResolution(image.HorizontalResolution, image.VerticalResolution); + edgeBitmap = Trace(image, edgeBitmap, 120); + + using (Graphics graphics = Graphics.FromImage(newImage)) + { + // Overlay the image. + graphics.DrawImage(edgeBitmap, 0, 0); + Rectangle rectangle = new Rectangle(0, 0, width, height); + + // Draw an edge around the image. + using (Pen blackPen = new Pen(Color.Black)) + { + blackPen.Width = 4; + graphics.DrawRectangle(blackPen, rectangle); + } + } + + edgeBitmap.Dispose(); + image.Dispose(); + image = newImage; + } + catch (Exception ex) + { + if (edgeBitmap != null) + { + edgeBitmap.Dispose(); + } + + if (newImage != null) + { + newImage.Dispose(); + } + + throw new ImageProcessingException("Error processing image with " + this.GetType().Name, ex); + } + + return image; + } + + /// + /// Traces the edges of a given . + /// TODO: Move this to another class. + /// + /// + /// The source . + /// + /// + /// The destination . + /// + /// + /// The threshold (between 0 and 255). + /// + /// + /// The a new instance of traced. + /// + private static Bitmap Trace(Image source, Image destination, byte threshold = 0) + { + int width = source.Width; + int height = source.Height; + + // Grab the edges converting to greyscale, and invert the colors. + ConvolutionFilter filter = new ConvolutionFilter(new SobelEdgeFilter(), true); + + using (Bitmap temp = filter.Process2DFilter(source)) + { + destination = new InvertMatrixFilter().TransformImage(temp, destination); + + // Darken it slightly to aid detection + destination = Adjustments.Brightness(destination, -5); + } + + // Loop through and replace any colors more white than the threshold + // with a transparent one. + using (FastBitmap destinationBitmap = new FastBitmap(destination)) + { + Parallel.For( + 0, + height, + y => + { + for (int x = 0; x < width; x++) + { + // ReSharper disable AccessToDisposedClosure + Color color = destinationBitmap.GetPixel(x, y); + if (color.B >= threshold) + { + destinationBitmap.SetPixel(x, y, Color.Transparent); + } + // ReSharper restore AccessToDisposedClosure + } + }); + } + + // Darken it again to average out the color. + destination = Adjustments.Brightness(destination, -5); + return (Bitmap)destination; + } + } +} diff --git a/src/ImageProcessor/Processors/Pixelate.cs b/src/ImageProcessor/Processors/Pixelate.cs index 7724f9b3ff..7138982eb7 100644 --- a/src/ImageProcessor/Processors/Pixelate.cs +++ b/src/ImageProcessor/Processors/Pixelate.cs @@ -69,9 +69,7 @@ namespace ImageProcessor.Processors { Tuple parameters = this.DynamicParameter; int size = parameters.Item1; - Rectangle rectangle = parameters.Item2.HasValue - ? parameters.Item2.Value - : new Rectangle(0, 0, image.Width, image.Height); + Rectangle rectangle = parameters.Item2 ?? new Rectangle(0, 0, image.Width, image.Height); int x = rectangle.X; int y = rectangle.Y; int offset = size / 2;