From 51edc8f606485be1df5e506189d50a092639632e Mon Sep 17 00:00:00 2001 From: James South Date: Tue, 3 Feb 2015 22:31:34 +0000 Subject: [PATCH] Experiments in CMYK haftoning Needs cleanup Former-commit-id: 88dd7a27cabb780526d1691d38a674210a174c7e Former-commit-id: d8e3f0a7f19517e0b52f30d021fbacc7a77eb498 --- src/ImageProcessor.Playground/Program.cs | 133 +++--- ...ack-Nicholson-Portrait.jpeg.REMOVED.git-id | 1 + .../images/input/cat.jpg.REMOVED.git-id | 1 + .../images/input/cmyk-test.png.REMOVED.git-id | 1 + .../images/input/cmyk.png | Bin 0 -> 37678 bytes .../images/input/mountain.jpg.REMOVED.git-id | 2 +- .../Imaging/ColorUnitTests.cs | 49 ++- src/ImageProcessor/ImageFactory.cs | 11 + src/ImageProcessor/ImageProcessor.csproj | 4 + .../Imaging/Colors/CmykColor.cs | 383 ++++++++++++++++++ .../Imaging/Colors/ColorExtensions.cs | 68 ++++ .../Imaging/Colors/HSLAColor.cs | 19 +- .../Imaging/Colors/RGBAColor.cs | 33 +- .../Imaging/Colors/YCbCrColor.cs | 16 + src/ImageProcessor/Imaging/FastBitmap.cs | 2 +- .../Filters/Artistic/HalftoneFilter.cs | 346 ++++++++++++++++ src/ImageProcessor/Imaging/Helpers/Effects.cs | 2 +- .../Imaging/Helpers/ImageMaths.cs | 46 ++- .../Imaging/Quantizers/Quantizer.cs | 4 +- .../Processors/Halftone - Copy.cs | 262 ++++++++++++ src/ImageProcessor/Processors/Halftone.cs | 181 +++++++++ src/ImageProcessor/Processors/Pixelate.cs | 4 +- 22 files changed, 1487 insertions(+), 81 deletions(-) create mode 100644 src/ImageProcessor.Playground/images/input/Martin-Schoeller-Jack-Nicholson-Portrait.jpeg.REMOVED.git-id create mode 100644 src/ImageProcessor.Playground/images/input/cat.jpg.REMOVED.git-id create mode 100644 src/ImageProcessor.Playground/images/input/cmyk-test.png.REMOVED.git-id create mode 100644 src/ImageProcessor.Playground/images/input/cmyk.png create mode 100644 src/ImageProcessor/Imaging/Colors/CmykColor.cs create mode 100644 src/ImageProcessor/Imaging/Colors/ColorExtensions.cs create mode 100644 src/ImageProcessor/Imaging/Filters/Artistic/HalftoneFilter.cs create mode 100644 src/ImageProcessor/Processors/Halftone - Copy.cs create mode 100644 src/ImageProcessor/Processors/Halftone.cs 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 0000000000000000000000000000000000000000..e5c625d433371a48aedc3f678babf5e666e8bf1f GIT binary patch literal 37678 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 + /// 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;