diff --git a/README.md b/README.md
index f3af515..efe61b1 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
# Dark Colors
@@ -88,4 +88,30 @@ var twoColorsCombined = ColorBlender.Combine(baseColor, colorToAdd);
var baseColor = Color.FromArgb(0, 0, 0);
var colorToAdd = Color.FromArgb(125, 55, 13);
var twoColorsCombined = baseColor.Combine(colorToAdd);
-```
\ No newline at end of file
+```
+
+## Color analyzer
+
+⚠️ Warning: this feature is pretty slow at the moment. It can process roughly one mega pixel per second, so it's not recommended to use it on large images.
+
+Find dominant color in an image.
+
+
+
+### Basic usage
+```csharp
+//array of pixels that represents an image
+Color[] pixels;
+//returns a list of dominant color candidates, ordered by probability
+List candidates = ColorAnalyzer.FindDominantColors(pixels);
+```
+
+### Advanced (with configuration)
+```csharp
+Color[] pixels;
+List candidates = ColorAnalyzer.FindDominantColors(pixels, options =>
+{
+ options.MinSaturation = 0.4f,
+ options.MinSpaceCoverage = 0.05f,
+ options.ColorGrouping = 0.3f
+});
\ No newline at end of file
diff --git a/assets/sample_screenshot_analyzer.png b/assets/sample_screenshot_analyzer.png
new file mode 100644
index 0000000..3281f73
Binary files /dev/null and b/assets/sample_screenshot_analyzer.png differ
diff --git a/sample/SampleApp/BitmapUtils.cs b/sample/SampleApp/BitmapUtils.cs
new file mode 100644
index 0000000..ec768f7
--- /dev/null
+++ b/sample/SampleApp/BitmapUtils.cs
@@ -0,0 +1,32 @@
+using SkiaSharp.Views.Desktop;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Windows.Media.Imaging;
+
+namespace SampleApp;
+
+public static class BitmapUtils
+{
+ public static Color[] GetPixels(Bitmap bitmap)
+ {
+ return bitmap.ToSKBitmap().Pixels
+ .Select(x => x.ToDrawingColor())
+ .ToArray();
+ }
+
+ public static BitmapImage BitmapToImageSource(Bitmap bitmap)
+ {
+ using MemoryStream memory = new MemoryStream();
+
+ bitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Bmp);
+ memory.Position = 0;
+ BitmapImage bitmapimage = new BitmapImage();
+ bitmapimage.BeginInit();
+ bitmapimage.StreamSource = memory;
+ bitmapimage.CacheOption = BitmapCacheOption.OnLoad;
+ bitmapimage.EndInit();
+
+ return bitmapimage;
+ }
+}
\ No newline at end of file
diff --git a/sample/SampleApp/ColorAnalyzerOptionsExample.cs b/sample/SampleApp/ColorAnalyzerOptionsExample.cs
new file mode 100644
index 0000000..cc63bff
--- /dev/null
+++ b/sample/SampleApp/ColorAnalyzerOptionsExample.cs
@@ -0,0 +1,58 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using DarkColors;
+using System.Collections.ObjectModel;
+using System.Drawing;
+using System.Linq;
+using System.Windows.Media.Imaging;
+
+namespace SampleApp;
+
+public partial class ColorAnalyzerExample : ObservableObject
+{
+ public class DemoImage
+ {
+ public BitmapImage? BitmapSource { get; set; }
+ public Color[] Pixels { get; set; }
+ public ObservableCollection Candidates { get; set; } = new();
+ }
+
+ public DominantColorAnalyzerOptions Options { get; } = new()
+ {
+ MaxCandidateCount = 5
+ };
+
+ public ObservableCollection Images { get; } = new();
+
+ public ColorAnalyzerExample(Bitmap[] bitmaps)
+ {
+ var images = bitmaps.Select(x => new DemoImage
+ {
+ BitmapSource = BitmapUtils.BitmapToImageSource(x),
+ Pixels = BitmapUtils.GetPixels(x)
+ });
+
+ foreach (var image in images)
+ {
+ Images.Add(image);
+ }
+
+ UpdateCandidates();
+ }
+
+ [RelayCommand]
+ private void UpdateCandidates()
+ {
+ foreach (var result in Images)
+ {
+ result.Candidates.Clear();
+
+ var candidates = ColorAnalyzer.FindDominantColors(result.Pixels, Options);
+
+ foreach (var candidate in candidates)
+ {
+ result.Candidates.Add(candidate);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample/SampleApp/ColorBlendingExample.cs b/sample/SampleApp/ColorBlendingExample.cs
index bdb9a7a..871713f 100644
--- a/sample/SampleApp/ColorBlendingExample.cs
+++ b/sample/SampleApp/ColorBlendingExample.cs
@@ -17,4 +17,4 @@ public ColorBlendingExample(string Title, Color baseColor, params ColorLayer[] l
Layers = layers;
ResultColor = ColorBlender.Combine(baseColor, layers);
}
-}
+}
\ No newline at end of file
diff --git a/sample/SampleApp/MainViewModel.cs b/sample/SampleApp/MainViewModel.cs
index 8229202..4701d59 100644
--- a/sample/SampleApp/MainViewModel.cs
+++ b/sample/SampleApp/MainViewModel.cs
@@ -1,4 +1,5 @@
using DarkColors;
+using SampleApp.Properties;
using System.Collections.Generic;
using System.Drawing;
@@ -6,29 +7,34 @@ namespace SampleApp;
public class MainViewModel
{
- public List Examples { get; } = new();
+ public List ColorBlendingExamples { get; } = new();
+ public ColorAnalyzerExample ColorAnalyzerExample { get; }
public MainViewModel()
{
- Examples.Add(new ColorBlendingExample("A bit of gray", Hex("#000"), new ColorLayer(Hex("#fff"), 25)));
- Examples.Add(new ColorBlendingExample("Fifty shades", Hex("#000"), new ColorLayer(Hex("#fff"), 50)));
- Examples.Add(new ColorBlendingExample("Bright green", Hex("#007d40"), new ColorLayer(Hex("#fff"), 75)));
+ ColorBlendingExamples.Add(new ColorBlendingExample("A bit of gray", Hex("#000"), new ColorLayer(Hex("#fff"), 25)));
+ ColorBlendingExamples.Add(new ColorBlendingExample("Fifty shades", Hex("#000"), new ColorLayer(Hex("#fff"), 50)));
+ ColorBlendingExamples.Add(new ColorBlendingExample("Bright green", Hex("#007d40"), new ColorLayer(Hex("#fff"), 75)));
- Examples.Add(new ColorBlendingExample("Is it blue or purple?", Hex("#000"), new ColorLayer(Hex("#4056F4"), 60)));
+ ColorBlendingExamples.Add(new ColorBlendingExample("Is it blue or purple?", Hex("#000"), new ColorLayer(Hex("#4056F4"), 60)));
- Examples.Add(new ColorBlendingExample("More than two colors", Hex("#007d40"), new ColorLayer[]
+ ColorBlendingExamples.Add(new ColorBlendingExample("More than two colors", Hex("#007d40"), new ColorLayer[]
{
new ColorLayer(Hex("#4056F4"), 42),
new ColorLayer(Hex("#B1740F"), 28)
}));
- Examples.Add(new ColorBlendingExample("Both alpha transparency and amount percentage used", Hex("#000"), new ColorLayer(Hex("#804056F4"), 55)));
+ ColorBlendingExamples.Add(new ColorBlendingExample("Both alpha transparency and amount percentage used", Hex("#000"), new ColorLayer(Hex("#804056F4"), 55)));
- Examples.Add(new ColorBlendingExample("Combo - both alpha transparency and amount percentage used for multiple colors", Hex("#007d40"), new ColorLayer[]
+ ColorBlendingExamples.Add(new ColorBlendingExample("Combo - both alpha transparency and amount percentage used for multiple colors", Hex("#007d40"), new ColorLayer[]
{
new ColorLayer(Hex("#804056F4"), 71),
new ColorLayer(Hex("#5CB1740F"), 20)
}));
+
+ var demoBitmaps = new[] { Resources.vildhjarta, Resources.rivers, Resources.abovebelow, Resources.greylotus, Resources.currents, Resources.dali, Resources.metallica, Resources.northlane };
+
+ ColorAnalyzerExample = new(demoBitmaps);
}
private static Color Hex(string hex)
diff --git a/sample/SampleApp/MainWindow.xaml b/sample/SampleApp/MainWindow.xaml
index f8a924a..930c742 100644
--- a/sample/SampleApp/MainWindow.xaml
+++ b/sample/SampleApp/MainWindow.xaml
@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SampleApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:skiaWpf="clr-namespace:SkiaSharp.Views.WPF;assembly=SkiaSharp.Views.WPF"
Title="Dark Colors Demo"
Width="800"
Height="450"
@@ -15,139 +16,292 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Padding="10,0"
+ Command="{Binding ColorAnalyzerExample.UpdateCandidatesCommand}"
+ Content="Update results" />
+
+
+
+
+
-
-
+ Margin="10,10,10,0"
+ Padding="10"
+ Background="#363636"
+ CornerRadius="10">
+
+
+ Margin="0,0,0,5"
+ Foreground="#fff"
+ Text="Source image" />
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/sample/SampleApp/Properties/Resources.Designer.cs b/sample/SampleApp/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..7186330
--- /dev/null
+++ b/sample/SampleApp/Properties/Resources.Designer.cs
@@ -0,0 +1,143 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace SampleApp.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SampleApp.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap abovebelow {
+ get {
+ object obj = ResourceManager.GetObject("abovebelow", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap currents {
+ get {
+ object obj = ResourceManager.GetObject("currents", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap dali {
+ get {
+ object obj = ResourceManager.GetObject("dali", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap greylotus {
+ get {
+ object obj = ResourceManager.GetObject("greylotus", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap metallica {
+ get {
+ object obj = ResourceManager.GetObject("metallica", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap northlane {
+ get {
+ object obj = ResourceManager.GetObject("northlane", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap rivers {
+ get {
+ object obj = ResourceManager.GetObject("rivers", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap vildhjarta {
+ get {
+ object obj = ResourceManager.GetObject("vildhjarta", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+ }
+}
diff --git a/sample/SampleApp/Properties/Resources.resx b/sample/SampleApp/Properties/Resources.resx
new file mode 100644
index 0000000..72261b5
--- /dev/null
+++ b/sample/SampleApp/Properties/Resources.resx
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ ..\Resources\abovebelow.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\currents.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\dali.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\greylotus.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\metallica.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\northlane.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\rivers.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\vildhjarta.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
\ No newline at end of file
diff --git a/sample/SampleApp/Resources/abovebelow.jpg b/sample/SampleApp/Resources/abovebelow.jpg
new file mode 100644
index 0000000..0a3b301
Binary files /dev/null and b/sample/SampleApp/Resources/abovebelow.jpg differ
diff --git a/sample/SampleApp/Resources/currents.jpg b/sample/SampleApp/Resources/currents.jpg
new file mode 100644
index 0000000..0f48922
Binary files /dev/null and b/sample/SampleApp/Resources/currents.jpg differ
diff --git a/sample/SampleApp/Resources/dali.jpg b/sample/SampleApp/Resources/dali.jpg
new file mode 100644
index 0000000..3e37537
Binary files /dev/null and b/sample/SampleApp/Resources/dali.jpg differ
diff --git a/sample/SampleApp/Resources/greylotus.jpg b/sample/SampleApp/Resources/greylotus.jpg
new file mode 100644
index 0000000..1fb0a8d
Binary files /dev/null and b/sample/SampleApp/Resources/greylotus.jpg differ
diff --git a/sample/SampleApp/Resources/metallica.jpg b/sample/SampleApp/Resources/metallica.jpg
new file mode 100644
index 0000000..cc92b96
Binary files /dev/null and b/sample/SampleApp/Resources/metallica.jpg differ
diff --git a/sample/SampleApp/Resources/northlane.jpg b/sample/SampleApp/Resources/northlane.jpg
new file mode 100644
index 0000000..22f2612
Binary files /dev/null and b/sample/SampleApp/Resources/northlane.jpg differ
diff --git a/sample/SampleApp/Resources/rivers.jpg b/sample/SampleApp/Resources/rivers.jpg
new file mode 100644
index 0000000..95f040a
Binary files /dev/null and b/sample/SampleApp/Resources/rivers.jpg differ
diff --git a/sample/SampleApp/Resources/vildhjarta.jpg b/sample/SampleApp/Resources/vildhjarta.jpg
new file mode 100644
index 0000000..d08523d
Binary files /dev/null and b/sample/SampleApp/Resources/vildhjarta.jpg differ
diff --git a/sample/SampleApp/SampleApp.csproj b/sample/SampleApp/SampleApp.csproj
index d46a286..3ffc835 100644
--- a/sample/SampleApp/SampleApp.csproj
+++ b/sample/SampleApp/SampleApp.csproj
@@ -11,4 +11,26 @@
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
diff --git a/src/DarkColors/ColorAnalyzer.cs b/src/DarkColors/ColorAnalyzer.cs
new file mode 100644
index 0000000..5d6daa4
--- /dev/null
+++ b/src/DarkColors/ColorAnalyzer.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+
+namespace DarkColors;
+
+///
+/// Utility for finding a dominant color within an image
+///
+public static class ColorAnalyzer
+{
+ ///
+ /// Find the dominant color in an image (array of pixels)
+ ///
+ /// Pixels of the image
+ /// Configuration
+ /// A list of dominant color candidates
+ ///
+ public static List FindDominantColors(Color[] pixels, Action? configurator = null)
+ {
+ var options = new DominantColorAnalyzerOptions();
+ configurator?.Invoke(options);
+
+ return FindDominantColors(pixels, options);
+ }
+
+ ///
+ /// Find the dominant color in an image (array of pixels)
+ ///
+ /// Pixels of the image
+ /// A list of dominant color candidates
+ ///
+ public static List FindDominantColors(Color[] pixels)
+ {
+ return FindDominantColors(pixels, new DominantColorAnalyzerOptions());
+ }
+
+ ///
+ /// Find the dominant color in an image (array of pixels)
+ ///
+ /// Pixels of the image
+ /// Configuration
+ /// A list of dominant color candidates
+ ///
+ ///
+ ///
+ public static List FindDominantColors(Color[] pixels, DominantColorAnalyzerOptions options)
+ {
+ if (pixels is null)
+ {
+ throw new ArgumentNullException(nameof(pixels));
+ }
+
+ if (pixels.Length == 0)
+ {
+ throw new ArgumentException($"{nameof(pixels)} cannot be empty", nameof(pixels));
+ }
+
+ if (options is null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ options.Validate();
+
+ var roundTo = (int)Math.Clamp(options.ColorGrouping * 100, 1, 100);
+
+ var candidates = pixels
+ .Select(x => new
+ {
+ Color = x,
+ AverageColor = Color.FromArgb(RoundToAndClamp(x.R, roundTo, 0, 255), RoundToAndClamp(x.G, roundTo, 0, 255), RoundToAndClamp(x.B, roundTo, 0, 255)),
+ })
+ .GroupBy(x => x.AverageColor)
+ .Take(100)
+ .Select(x => DominantColorCandidate.Create(x.Select(c => c.Color), x.Count(), pixels.Length))
+ .OrderByDescending(x => x.SpaceCoverage)
+ .ThenByDescending(x => x.NonGreyscaleScore);
+
+ var filtered = candidates
+ .Where(x => x.Brightness >= options.MinBrightness && x.Brightness <= options.MaxBrightness)
+ .Where(x => x.Saturation >= options.MinSaturation && x.Saturation <= options.MaxSaturation)
+ .Where(x => x.SpaceCoverage > options.MinSpaceCoverage)
+ .Where(x => x.NonGreyscaleScore > options.MinNonGreyscaleScore)
+ .Take(options.MaxCandidateCount)
+ .ToList();
+
+ if (!filtered.Any() && candidates.Any())
+ {
+ return new List { candidates.First() };
+ }
+
+ return filtered;
+ }
+
+ internal static float GetNonGreyscaleScore(Color color)
+ {
+ var r = Math.Abs(color.R - color.G);
+ var g = Math.Abs(color.G - color.B);
+ var b = Math.Abs(color.B - color.R);
+
+ var nonGreyscaleScore = (float)(r + g + b) / (255 * 3);
+
+ return nonGreyscaleScore;
+ }
+
+ internal static float GetSpaceCoverage(int count, int totalCount)
+ {
+ return (float)count / totalCount;
+ }
+
+ private static int RoundToAndClamp(int number, int roundTo, int min, int max)
+ {
+ var rounded = Math.Round((double)number / roundTo, 0, MidpointRounding.ToZero) * roundTo;
+ return Math.Clamp((int)rounded, min, max);
+ }
+}
\ No newline at end of file
diff --git a/src/DarkColors/ColorBlender.cs b/src/DarkColors/ColorBlender.cs
index 9ebdef8..9812ac0 100644
--- a/src/DarkColors/ColorBlender.cs
+++ b/src/DarkColors/ColorBlender.cs
@@ -1,4 +1,5 @@
-using System.Drawing;
+using System;
+using System.Drawing;
namespace DarkColors;
diff --git a/src/DarkColors/ColorLayer.cs b/src/DarkColors/ColorLayer.cs
index 0b20b87..34e8efd 100644
--- a/src/DarkColors/ColorLayer.cs
+++ b/src/DarkColors/ColorLayer.cs
@@ -1,4 +1,5 @@
-using System.Drawing;
+using System;
+using System.Drawing;
namespace DarkColors;
@@ -18,6 +19,12 @@ public class ColorLayer
///
public int AmountPercentage { get; }
+ ///
+ /// Creates an instance of a
+ ///
+ /// The color associated with the layer
+ /// The amount of the color to use in percent (0-100)
+ /// Thrown when is out of range (0-100)
public ColorLayer(Color color, int amountPercentage)
{
if(amountPercentage < 0 || amountPercentage > 100)
diff --git a/src/DarkColors/DarkColors.csproj b/src/DarkColors/DarkColors.csproj
index bd5d037..668daee 100644
--- a/src/DarkColors/DarkColors.csproj
+++ b/src/DarkColors/DarkColors.csproj
@@ -1,15 +1,14 @@
- net6.0
- enable
+ net6.0;net7.0
enable
True
Divis.DarkColors
- 1.0.2
+ 1.1.0
michaldivis
Michal Diviš
Dark Colors
diff --git a/src/DarkColors/DominantColorAnalyzerOptions.cs b/src/DarkColors/DominantColorAnalyzerOptions.cs
new file mode 100644
index 0000000..e9341ae
--- /dev/null
+++ b/src/DarkColors/DominantColorAnalyzerOptions.cs
@@ -0,0 +1,104 @@
+using System;
+
+namespace DarkColors;
+
+///
+/// Configuration options for the color analyzer
+///
+public class DominantColorAnalyzerOptions
+{
+ ///
+ /// The minimum brightness of a color to be considered
+ /// Allowed range: 0-1
+ ///
+ public float MinBrightness { get; set; } = 0.1f;
+
+ ///
+ /// The maximum brightness of a color to be considered
+ /// Allowed range: 0-1
+ ///
+ public float MaxBrightness { get; set; } = 0.9f;
+
+ ///
+ /// The minimum saturation of a color to be considered
+ /// Allowed range: 0-1
+ ///
+ public float MinSaturation { get; set; } = 0.25f;
+
+ ///
+ /// The maximum saturation of a color to be considered
+ /// Allowed range: 0-1
+ ///
+ public float MaxSaturation { get; set; } = 1f;
+
+ ///
+ /// The minimum non-greyscale score of a color to be considered. The greater the non-greyscale score, the further the color is from a gray-ish tone... the more colorful it is
+ /// Allowed range: 0-1
+ ///
+ public float MinNonGreyscaleScore { get; set; } = 0.1f;
+
+ ///
+ /// The minimum space coverage (percentage of space occupied on the image) of a color to be considered
+ /// Allowed range: 0-1
+ ///
+ public float MinSpaceCoverage { get; set; } = 0f;
+
+ ///
+ /// The amount of "color grouping". The greater the value, the more will similar colors be squashed together and treated as one. The lesser the value, the more will similar colors be treated as distinct colors, ever when they're almost the same.
+ /// Allowed range: 0-1
+ ///
+ public float ColorGrouping { get; set; } = 0.2f;
+
+ ///
+ /// Maximum amount of canidates to return
+ /// Allowed range: 1-100
+ ///
+ public int MaxCandidateCount { get; set; } = 10;
+
+ ///
+ /// Performs validation on the values and throws if any validation rules aren't met
+ ///
+ ///
+ public void Validate()
+ {
+ if(MinBrightness < 0f || MinBrightness > 1f)
+ {
+ throw new ArgumentOutOfRangeException(nameof(MinBrightness), MinBrightness, $"{nameof(MinBrightness)} has to be within range 0-1");
+ }
+
+ if (MaxBrightness < 0f || MaxBrightness > 1f)
+ {
+ throw new ArgumentOutOfRangeException(nameof(MaxBrightness), MaxBrightness, $"{nameof(MaxBrightness)} has to be within range 0-1");
+ }
+
+ if (MinSaturation < 0f || MinSaturation > 1f)
+ {
+ throw new ArgumentOutOfRangeException(nameof(MinSaturation), MinSaturation, $"{nameof(MinSaturation)} has to be within range 0-1");
+ }
+
+ if (MaxSaturation < 0f || MaxSaturation > 1f)
+ {
+ throw new ArgumentOutOfRangeException(nameof(MaxSaturation), MaxSaturation, $"{nameof(MaxSaturation)} has to be within range 0-1");
+ }
+
+ if (MinNonGreyscaleScore < 0f || MinNonGreyscaleScore > 1f)
+ {
+ throw new ArgumentOutOfRangeException(nameof(MinNonGreyscaleScore), MinNonGreyscaleScore, $"{nameof(MinNonGreyscaleScore)} has to be within range 0-1");
+ }
+
+ if (MinSpaceCoverage < 0f || MinSpaceCoverage > 1f)
+ {
+ throw new ArgumentOutOfRangeException(nameof(MinSpaceCoverage), MinSpaceCoverage, $"{nameof(MinSpaceCoverage)} has to be within range 0-1");
+ }
+
+ if (ColorGrouping < 0f || ColorGrouping > 1f)
+ {
+ throw new ArgumentOutOfRangeException(nameof(ColorGrouping), ColorGrouping, $"{nameof(ColorGrouping)} has to be within range 0-1");
+ }
+
+ if (MaxCandidateCount < 1 || MaxCandidateCount > 100)
+ {
+ throw new ArgumentOutOfRangeException(nameof(MaxCandidateCount), MaxCandidateCount, $"{nameof(MaxCandidateCount)} has to be within range 1-100");
+ }
+ }
+}
diff --git a/src/DarkColors/DominantColorCandidate.cs b/src/DarkColors/DominantColorCandidate.cs
new file mode 100644
index 0000000..46fefe0
--- /dev/null
+++ b/src/DarkColors/DominantColorCandidate.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+
+namespace DarkColors;
+
+///
+/// A potential dominant color candidate
+///
+/// The color
+/// The brightness the a color
+/// The saturation of the color
+/// The amount of space in the image the color covers
+/// The greater the non-greyscale score, the further the color is from a gray-ish tone... the more colorful it is
+public record DominantColorCandidate(Color Color, float Brightness, float Saturation, float SpaceCoverage, float NonGreyscaleScore)
+{
+ internal static DominantColorCandidate Create(IEnumerable colors, int count, int totalCount)
+ {
+ var averageColor = Color.FromArgb((int)colors.Average(x => x.R), (int)colors.Average(x => x.G), (int)colors.Average(x => x.B));
+
+ return new DominantColorCandidate(averageColor, averageColor.GetBrightness(), averageColor.GetSaturation(), ColorAnalyzer.GetSpaceCoverage(count, totalCount), ColorAnalyzer.GetNonGreyscaleScore(averageColor));
+ }
+}
diff --git a/tests/DarkColorsTests/ColorAnalyzerTests.cs b/tests/DarkColorsTests/ColorAnalyzerTests.cs
new file mode 100644
index 0000000..b3d9492
--- /dev/null
+++ b/tests/DarkColorsTests/ColorAnalyzerTests.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Diagnostics;
+using System.Drawing;
+
+namespace DarkColors;
+
+public class ColorAnalyzerTests
+{
+ private readonly Color[] _pixels = new[] { Color.Black, Color.Gray, Color.White, Color.Green, Color.Red };
+
+ [Fact]
+ public void FindDominantColors_ShouldThrow_WhenPixelsNull()
+ {
+ Action act = () => ColorAnalyzer.FindDominantColors(null!);
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void FindDominantColors_ShouldThrow_WhenPixelsEmpty()
+ {
+ Action act = () => ColorAnalyzer.FindDominantColors(Array.Empty());
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void FindDominantColors_ShouldThrow_WhenOptionsNull()
+ {
+ Action act = () => ColorAnalyzer.FindDominantColors(_pixels, (DominantColorAnalyzerOptions)null!);
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void FindDominantColors_ShouldWork()
+ {
+ var candidates = ColorAnalyzer.FindDominantColors(_pixels);
+
+ candidates.Should().Contain(c => CompareColors(c.Color, Color.Green));
+ candidates.Should().Contain(c => CompareColors(c.Color, Color.Red));
+
+ candidates.Should().NotContain(c => CompareColors(c.Color, Color.Black));
+ candidates.Should().NotContain(c => CompareColors(c.Color, Color.Gray));
+ candidates.Should().NotContain(c => CompareColors(c.Color, Color.White));
+ }
+
+ private bool CompareColors(Color a, Color b)
+ {
+ return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B;
+ }
+}
\ No newline at end of file
diff --git a/tests/DarkColorsTests/ColorLayerTests.cs b/tests/DarkColorsTests/ColorLayerTests.cs
index b03f829..cb0eafd 100644
--- a/tests/DarkColorsTests/ColorLayerTests.cs
+++ b/tests/DarkColorsTests/ColorLayerTests.cs
@@ -1,4 +1,5 @@
-using System.Drawing;
+using System;
+using System.Drawing;
namespace DarkColors;
public class ColorLayerTests
@@ -22,4 +23,4 @@ public void Ctor_ShouldThrow_WhenAmountAboveHundred()
Action act = () => new ColorLayer(Color.Black, 101);
_ = act.Should().Throw();
}
-}
+}
\ No newline at end of file
diff --git a/tests/DarkColorsTests/DarkColorsTests.csproj b/tests/DarkColorsTests/DarkColorsTests.csproj
index caa09e3..e5bda1b 100644
--- a/tests/DarkColorsTests/DarkColorsTests.csproj
+++ b/tests/DarkColorsTests/DarkColorsTests.csproj
@@ -1,8 +1,7 @@
-
+
net6.0
- enable
enable
false