diff --git a/Pinta.Effects/Classes/ErrorDiffusionMatrix.cs b/Pinta.Effects/Classes/ErrorDiffusionMatrix.cs
new file mode 100644
index 000000000..9d8e1f84b
--- /dev/null
+++ b/Pinta.Effects/Classes/ErrorDiffusionMatrix.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using Pinta.Gui.Widgets;
+
+namespace Pinta.Effects;
+
+public enum PredefinedDiffusionMatrices
+{
+ // Translators: Image dithering matrix named after Frankie Sierra
+ [Caption ("Sierra")]
+ Sierra,
+
+ // Translators: Image dithering matrix named after Frankie Sierra
+ [Caption ("Two-Row Sierra")]
+ TwoRowSierra,
+
+ // Translators: Image dithering matrix named after Frankie Sierra
+ [Caption ("Sierra Lite")]
+ SierraLite,
+
+ // Translators: Image dithering matrix named after Daniel Burkes
+ [Caption ("Burkes")]
+ Burkes,
+
+ // Translators: Image dithering matrix named after Bill Atkinson
+ [Caption ("Atkinson")]
+ Atkinson,
+
+ // Translators: Image dithering matrix named after Peter Stucki
+ [Caption ("Stucki")]
+ Stucki,
+
+ // Translators: Image dithering matrix named after J. F. Jarvis, C. N. Judice, and W. H. Ninke
+ [Caption ("Jarvis-Judice-Ninke")]
+ JarvisJudiceNinke,
+
+ // Translators: Image dithering matrix named after Robert W. Floyd and Louis Steinberg
+ [Caption ("Floyd-Steinberg")]
+ FloydSteinberg,
+
+ // Translators: Image dithering matrix named after Robert W. Floyd and Louis Steinberg. Some software may use it and call it Floyd-Steinberg, but it's not the actual Floyd-Steinberg matrix
+ [Caption ("Floyd-Steinberg Lite")]
+ FalseFloydSteinberg,
+}
+
+///
+/// Represents the matrix that is used by the dithering algorithm
+/// in order to propagate the error (defined as the difference
+/// between a pixel's color and the color in a certain palette that is
+/// closest to it) forward.
+///
+internal sealed class ErrorDiffusionMatrix
+{
+ public static ErrorDiffusionMatrix GetPredefined (PredefinedDiffusionMatrices choice)
+ {
+ return choice switch {
+ PredefinedDiffusionMatrices.Sierra => Predefined.Sierra,
+ PredefinedDiffusionMatrices.TwoRowSierra => Predefined.TwoRowSierra,
+ PredefinedDiffusionMatrices.SierraLite => Predefined.SierraLite,
+ PredefinedDiffusionMatrices.Burkes => Predefined.Burkes,
+ PredefinedDiffusionMatrices.Atkinson => Predefined.Atkinson,
+ PredefinedDiffusionMatrices.Stucki => Predefined.Stucki,
+ PredefinedDiffusionMatrices.JarvisJudiceNinke => Predefined.JarvisJudiceNinke,
+ PredefinedDiffusionMatrices.FloydSteinberg => Predefined.FloydSteinberg,
+ PredefinedDiffusionMatrices.FalseFloydSteinberg => Predefined.FakeFloydSteinberg,
+ _ => throw new InvalidEnumArgumentException (nameof (choice), (int) choice, typeof (PredefinedDiffusionMatrices)),
+ };
+ }
+
+ public static class Predefined
+ {
+ public static ErrorDiffusionMatrix Sierra { get; } = new ErrorDiffusionMatrix (DefaultMatrixArrays.Sierra, 2);
+ public static ErrorDiffusionMatrix TwoRowSierra { get; } = new ErrorDiffusionMatrix (DefaultMatrixArrays.TwoRowSierra, 2);
+ public static ErrorDiffusionMatrix SierraLite { get; } = new ErrorDiffusionMatrix (DefaultMatrixArrays.SierraLite, 1);
+ public static ErrorDiffusionMatrix Burkes { get; } = new ErrorDiffusionMatrix (DefaultMatrixArrays.Burkes, 2);
+ public static ErrorDiffusionMatrix Atkinson { get; } = new ErrorDiffusionMatrix (DefaultMatrixArrays.Atkinson, 1);
+ public static ErrorDiffusionMatrix Stucki { get; } = new ErrorDiffusionMatrix (DefaultMatrixArrays.Stucki, 2);
+ public static ErrorDiffusionMatrix JarvisJudiceNinke { get; } = new ErrorDiffusionMatrix (DefaultMatrixArrays.JarvisJudiceNinke, 2);
+ public static ErrorDiffusionMatrix FloydSteinberg { get; } = new ErrorDiffusionMatrix (DefaultMatrixArrays.FloydSteinberg, 1);
+ public static ErrorDiffusionMatrix FakeFloydSteinberg { get; } = new ErrorDiffusionMatrix (DefaultMatrixArrays.FakeFloydSteinberg, 0);
+ }
+
+ private static class DefaultMatrixArrays
+ {
+ public static int[,] Sierra { get; } = {
+ { 0, 0, 0, 5, 3, },
+ { 2, 4, 5, 4, 2, },
+ { 0, 2, 3, 2, 0, },
+ };
+
+ public static int[,] TwoRowSierra { get; } = {
+ { 0, 0, 0, 4, 3, },
+ { 1, 2, 3, 2, 1, },
+ };
+
+ public static int[,] SierraLite { get; } = {
+ { 0, 0, 2, },
+ { 1, 1, 0, },
+ };
+
+ public static int[,] Burkes { get; } = {
+ { 0, 0, 0, 8, 4, },
+ { 2, 4, 8, 4, 2, },
+ };
+
+ public static int[,] Atkinson { get; } = {
+ { 0, 0, 1, 1, },
+ { 1, 1, 1, 0, },
+ { 0, 1, 0, 0, },
+ };
+
+ public static int[,] Stucki { get; } = {
+ { 0, 0, 0, 8, 4, },
+ { 2, 4, 8, 4, 2, },
+ { 1, 2, 4, 2, 1, },
+ };
+
+ public static int[,] JarvisJudiceNinke { get; } = {
+ { 0, 0, 0, 7, 5, },
+ { 3, 5, 7, 5, 3, },
+ { 1, 3, 5, 3, 1, },
+ };
+
+ public static int[,] FloydSteinberg { get; } = {
+ { 0, 0, 7, },
+ { 3, 5, 1, }
+ };
+
+ public static int[,] FakeFloydSteinberg { get; } = {
+ { 0, 3, },
+ { 3, 2, }
+ };
+ }
+
+ private readonly int[,] array_2_d;
+ public int Columns { get; }
+ public int Rows { get; }
+ public int TotalWeight { get; }
+ public int ColumnsToLeft { get; }
+ public int ColumnsToRight { get; }
+ public int RowsBelow { get; }
+ public int this[int row, int column] => array_2_d[row, column];
+ public ErrorDiffusionMatrix (int[,] array2D, int pixelColumn)
+ {
+ var clone = (int[,]) array2D.Clone ();
+ var rows = clone.GetLength (0);
+ if (rows <= 0) throw new ArgumentException ("Array has to have a strictly positive number of rows", nameof (array2D));
+ var columns = clone.GetLength (1);
+ if (columns <= 0) throw new ArgumentException ("Array has to have a strictly positive number of rows", nameof (array2D));
+ if (pixelColumn < 0) throw new ArgumentException ("Argument has to refer to a valid column offset", nameof (pixelColumn));
+ if (pixelColumn >= columns) throw new ArgumentException ("Argument has to refer to a valid column offset", nameof (pixelColumn));
+ if (clone[0, pixelColumn] != 0) throw new ArgumentException ("Target pixel cannot have a nonzero weight");
+ var flattened = Flatten2DArray (clone);
+ if (flattened.Any (w => w < 0)) throw new ArgumentException ("No negative weights", nameof (array2D));
+ if (flattened.Take (pixelColumn).Any (w => w != 0)) throw new ArgumentException ("Pixels previous to target cannot have nonzero weights");
+ ColumnsToLeft = pixelColumn;
+ ColumnsToRight = columns - 1 - pixelColumn;
+ TotalWeight = flattened.Sum ();
+ Columns = columns;
+ Rows = rows;
+ RowsBelow = rows - 1;
+ array_2_d = clone;
+ }
+
+ private static IEnumerable Flatten2DArray (T[,] array)
+ {
+ for (int i = 0; i < array.GetLength (0); i++)
+ for (int j = 0; j < array.GetLength (1); j++)
+ yield return array[i, j];
+ }
+}
diff --git a/Pinta.Effects/CoreEffectsExtension.cs b/Pinta.Effects/CoreEffectsExtension.cs
index 21efe9533..cdadd3664 100644
--- a/Pinta.Effects/CoreEffectsExtension.cs
+++ b/Pinta.Effects/CoreEffectsExtension.cs
@@ -60,6 +60,7 @@ public void Initialize ()
PintaCore.Effects.RegisterEffect (new CloudsEffect (services));
PintaCore.Effects.RegisterEffect (new EdgeDetectEffect ());
PintaCore.Effects.RegisterEffect (new EmbossEffect ());
+ PintaCore.Effects.RegisterEffect (new DitheringEffect ());
PintaCore.Effects.RegisterEffect (new FragmentEffect ());
PintaCore.Effects.RegisterEffect (new FrostedGlassEffect ());
PintaCore.Effects.RegisterEffect (new GaussianBlurEffect ());
@@ -105,6 +106,7 @@ public void Uninitialize ()
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (CloudsEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (EdgeDetectEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (EmbossEffect));
+ PintaCore.Effects.UnregisterInstanceOfEffect (typeof (DitheringEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (FragmentEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (FrostedGlassEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (GaussianBlurEffect));
diff --git a/Pinta.Effects/Effects/DitheringEffect.cs b/Pinta.Effects/Effects/DitheringEffect.cs
new file mode 100644
index 000000000..a4ca0626e
--- /dev/null
+++ b/Pinta.Effects/Effects/DitheringEffect.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Immutable;
+using Cairo;
+using Pinta.Core;
+using Pinta.Gui.Widgets;
+
+namespace Pinta.Effects;
+
+public sealed class DitheringEffect : BaseEffect
+{
+ public override string Name => Translations.GetString ("Dithering");
+ public override bool IsConfigurable => true;
+ // TODO: Icon
+ public override string EffectMenuCategory => Translations.GetString ("Color");
+ public DitheringData Data => (DitheringData) EffectData!; // NRT - Set in constructor
+
+ public override bool IsTileable => false;
+
+ public DitheringEffect ()
+ {
+ EffectData = new DitheringData ();
+ }
+
+ public override void LaunchConfiguration ()
+ {
+ EffectHelper.LaunchSimpleEffectDialog (this);
+ }
+
+ private sealed record DitheringSettings (
+ ErrorDiffusionMatrix diffusionMatrix,
+ ImmutableArray palette,
+ int sourceWidth,
+ int sourceHeight);
+
+ private DitheringSettings CreateSettings (ImageSurface src)
+ => new (
+ diffusionMatrix: ErrorDiffusionMatrix.GetPredefined (Data.ErrorDiffusionMethod),
+ palette: PaletteHelper.GetPredefined (Data.PaletteChoice),
+ sourceWidth: src.Width,
+ sourceHeight: src.Height
+ );
+
+ protected override void Render (ImageSurface src, ImageSurface dest, RectangleI roi)
+ {
+ DitheringSettings settings = CreateSettings (src);
+
+ ReadOnlySpan src_data = src.GetReadOnlyPixelData ();
+ Span dst_data = dest.GetPixelData ();
+
+ for (int y = roi.Top; y <= roi.Bottom; y++) {
+ for (int x = roi.Left; x <= roi.Right; x++) {
+ int currentIndex = y * settings.sourceWidth + x;
+ dst_data[currentIndex] = src_data[currentIndex];
+ }
+ }
+
+ for (int y = roi.Top; y <= roi.Bottom; y++) {
+
+ for (int x = roi.Left; x <= roi.Right; x++) {
+
+ int currentIndex = y * settings.sourceWidth + x;
+ ColorBgra originalPixel = dst_data[currentIndex];
+ ColorBgra closestColor = FindClosestPaletteColor (settings.palette, originalPixel);
+
+ dst_data[currentIndex] = closestColor;
+
+ int errorRed = originalPixel.R - closestColor.R;
+ int errorGreen = originalPixel.G - closestColor.G;
+ int errorBlue = originalPixel.B - closestColor.B;
+
+ for (int r = 0; r < settings.diffusionMatrix.Rows; r++) {
+
+ for (int c = 0; c < settings.diffusionMatrix.Columns; c++) {
+
+ var weight = settings.diffusionMatrix[r, c];
+
+ if (weight <= 0)
+ continue;
+
+ PointI thisItem = new (
+ X: x + c - settings.diffusionMatrix.ColumnsToLeft,
+ Y: y + r
+ );
+
+ if (thisItem.X < roi.Left || thisItem.X >= roi.Right)
+ continue;
+
+ if (thisItem.Y < roi.Top || thisItem.Y >= roi.Bottom)
+ continue;
+
+ int idx = (thisItem.Y * settings.sourceWidth) + thisItem.X;
+
+ double factor = ((double) weight) / settings.diffusionMatrix.TotalWeight;
+
+ dst_data[idx] = AddError (dst_data[idx], factor, errorRed, errorGreen, errorBlue);
+ }
+ }
+
+ }
+ }
+ }
+
+ private static ColorBgra AddError (ColorBgra color, double factor, int errorRed, int errorGreen, int errorBlue)
+ => ColorBgra.FromBgra (
+ b: Utility.ClampToByte (color.B + (int) (factor * errorBlue)),
+ g: Utility.ClampToByte (color.G + (int) (factor * errorGreen)),
+ r: Utility.ClampToByte (color.R + (int) (factor * errorRed)),
+ a: 255
+ );
+
+ private static ColorBgra FindClosestPaletteColor (ImmutableArray palette, ColorBgra original)
+ {
+ if (palette.IsDefault) throw new ArgumentException ("Palette not initialized", nameof (palette));
+ if (palette.Length == 0) throw new ArgumentException ("Palette cannot be empty", nameof (palette));
+ if (palette.Length == 1) return palette[0];
+ double minDistance = double.MaxValue;
+ ColorBgra closestColor = ColorBgra.FromBgra (0, 0, 0, 1);
+ foreach (var paletteColor in palette) {
+ double distance = CalculateSquaredDistance (original, paletteColor);
+ if (distance >= minDistance) continue;
+ minDistance = distance;
+ closestColor = paletteColor;
+ }
+ return closestColor;
+ }
+
+ private static double CalculateSquaredDistance (ColorBgra color1, ColorBgra color2)
+ {
+ double deltaR = color1.R - color2.R;
+ double deltaG = color1.G - color2.G;
+ double deltaB = color1.B - color2.B;
+ return deltaR * deltaR + deltaG * deltaG + deltaB * deltaB;
+ }
+
+ public sealed class DitheringData : EffectData
+ {
+ [Caption ("Error Diffusion Method")]
+ public PredefinedDiffusionMatrices ErrorDiffusionMethod { get; set; } = PredefinedDiffusionMatrices.FloydSteinberg;
+
+ [Caption ("Palette")]
+ public PredefinedPalettes PaletteChoice { get; set; } = PredefinedPalettes.OldWindows16;
+ }
+}
diff --git a/Pinta.Effects/Utilities/PaletteHelper.cs b/Pinta.Effects/Utilities/PaletteHelper.cs
new file mode 100644
index 000000000..84a133940
--- /dev/null
+++ b/Pinta.Effects/Utilities/PaletteHelper.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.ComponentModel;
+using System.Linq;
+using Pinta.Core;
+
+namespace Pinta.Effects;
+
+public enum PredefinedPalettes
+{
+ BlackWhite,
+ OldMsPaint,
+ OldWindows16,
+ OldWindows20,
+ Rgb3Bit,
+ Rgb666,
+ Rgb6Bit,
+ Rgb12Bit,
+}
+
+internal static class PaletteHelper
+{
+ public static ImmutableArray GetPredefined (PredefinedPalettes choice)
+ {
+ return choice switch {
+ PredefinedPalettes.BlackWhite => Predefined.BlackWhite,
+ PredefinedPalettes.OldMsPaint => Predefined.OldMsPaint,
+ PredefinedPalettes.OldWindows16 => Predefined.OldWindows16,
+ PredefinedPalettes.OldWindows20 => Predefined.OldWindows20,
+ PredefinedPalettes.Rgb3Bit => Predefined.Rgb3Bit,
+ PredefinedPalettes.Rgb666 => Predefined.Rgb666,
+ PredefinedPalettes.Rgb6Bit => Predefined.Rgb6Bit,
+ PredefinedPalettes.Rgb12Bit => Predefined.Rgb12Bit,
+ _ => throw new InvalidEnumArgumentException (nameof (choice), (int) choice, typeof (PredefinedPalettes)),
+ };
+ }
+
+ public static class Predefined
+ {
+ public static ImmutableArray BlackWhite { get; }
+ public static ImmutableArray OldMsPaint => old_ms_paint.Value;
+ public static ImmutableArray OldWindows16 => old_windows_16.Value;
+ public static ImmutableArray OldWindows20 => old_windows_20.Value;
+ public static ImmutableArray Rgb3Bit => rgb_3_bit.Value;
+ public static ImmutableArray Rgb666 => rgb_666.Value;
+ public static ImmutableArray Rgb6Bit => rgb_6_bit.Value;
+ public static ImmutableArray Rgb12Bit => rgb_12_bit.Value;
+
+ private static readonly Lazy> old_windows_16;
+ private static readonly Lazy> old_windows_20;
+ private static readonly Lazy> old_ms_paint;
+ private static readonly Lazy> rgb_3_bit;
+ private static readonly Lazy> rgb_666;
+ private static readonly Lazy> rgb_6_bit;
+ private static readonly Lazy> rgb_12_bit;
+
+ static Predefined ()
+ {
+ BlackWhite = ImmutableArray.CreateRange (new[] { ColorBgra.Black, ColorBgra.White });
+ old_ms_paint = new (() => EnumerateOldMsPaintColors ().ToImmutableArray ());
+ old_windows_16 = new (() => EnumerateOldWindows16Colors ().ToImmutableArray ());
+ old_windows_20 = new (() => EnumerateOldWindows20Colors ().ToImmutableArray ());
+ rgb_3_bit = new (() => RgbColorCube (levels: 2, factor: 255).ToImmutableArray ()); // https://en.wikipedia.org/wiki/List_of_monochrome_and_RGB_color_formats#3-bit_RGB
+ rgb_666 = new (() => RgbColorCube (levels: 6, factor: 51).ToImmutableArray ()); // https://en.wikipedia.org/wiki/List_of_software_palettes#6_level_RGB
+ rgb_6_bit = new (() => RgbColorCube (levels: 4, factor: 85).ToImmutableArray ()); // https://en.wikipedia.org/wiki/List_of_monochrome_and_RGB_color_formats#6-bit_RGB
+ rgb_12_bit = new (() => RgbColorCube (levels: 16, factor: 17).ToImmutableArray ()); // https://en.wikipedia.org/wiki/List_of_monochrome_and_RGB_color_formats#12-bit_RGB
+ }
+
+ private static IEnumerable EnumerateOldWindows16Colors ()
+ {
+ // https://en.wikipedia.org/wiki/List_of_software_palettes#Microsoft_Windows_and_IBM_OS/2_default_16-color_palette
+ yield return ColorBgra.FromBgr (0, 0, 0); // Black
+ yield return ColorBgra.FromBgr (0, 0, 128); // Blue
+ yield return ColorBgra.FromBgr (0, 128, 0); // Green
+ yield return ColorBgra.FromBgr (0, 128, 128); // Cyan
+ yield return ColorBgra.FromBgr (128, 0, 0); // Red
+ yield return ColorBgra.FromBgr (128, 0, 128); // Magenta
+ yield return ColorBgra.FromBgr (128, 64, 0); // Brown
+ yield return ColorBgra.FromBgr (192, 192, 192); // Light Gray
+ yield return ColorBgra.FromBgr (128, 128, 128); // Dark Gray
+ yield return ColorBgra.FromBgr (0, 0, 255); // Light Blue
+ yield return ColorBgra.FromBgr (0, 255, 0); // Light Green
+ yield return ColorBgra.FromBgr (0, 255, 255); // Light Cyan
+ yield return ColorBgra.FromBgr (255, 0, 0); // Light Red
+ yield return ColorBgra.FromBgr (255, 0, 255); // Light Magenta
+ yield return ColorBgra.FromBgr (255, 255, 0); // Yellow
+ yield return ColorBgra.FromBgr (255, 255, 255); // White
+ }
+
+ private static IEnumerable EnumerateOldWindows20Colors ()
+ {
+ // https://en.wikipedia.org/wiki/List_of_software_palettes#Microsoft_Windows_default_20-color_palette
+ foreach (var color in EnumerateOldWindows16Colors ())
+ yield return color;
+
+ yield return ColorBgra.FromBgr (240, 251, 255); // Cream
+ yield return ColorBgra.FromBgr (192, 220, 192); // Money Green
+ yield return ColorBgra.FromBgr (240, 202, 166); // Sky Blue
+ yield return ColorBgra.FromBgr (164, 160, 160); // Medium Grey
+ }
+
+ private static IEnumerable EnumerateOldMsPaintColors ()
+ {
+ // https://wiki.vg-resource.com/Paint
+
+ yield return ColorBgra.FromBgr (0, 0, 0); // Black
+
+ yield return ColorBgra.FromBgr (0, 0, 128); // Maroon
+ yield return ColorBgra.FromBgr (0, 128, 0); // Green
+ yield return ColorBgra.FromBgr (0, 128, 128); // Olive
+ yield return ColorBgra.FromBgr (128, 0, 0); // Navy blue
+ yield return ColorBgra.FromBgr (128, 0, 128); // Magenta
+ yield return ColorBgra.FromBgr (128, 128, 0); // Teal
+ yield return ColorBgra.FromBgr (128, 128, 128); // Dark gray
+
+ yield return ColorBgra.FromBgr (0, 0, 255); // Red
+ yield return ColorBgra.FromBgr (0, 255, 0); // Lime green
+ yield return ColorBgra.FromBgr (0, 255, 255); // Yellow
+ yield return ColorBgra.FromBgr (255, 0, 0); // Blue
+ yield return ColorBgra.FromBgr (255, 0, 255); // Light Magenta
+ yield return ColorBgra.FromBgr (255, 255, 0); // Cyan
+ yield return ColorBgra.FromBgr (255, 255, 255); // White
+
+ yield return ColorBgra.FromBgr (0, 64, 128); // Saddle brown
+ yield return ColorBgra.FromBgr (64, 64, 0); // Cyprus (dark teal)
+ yield return ColorBgra.FromBgr (64, 128, 128); // Highball (mossy olive)
+ yield return ColorBgra.FromBgr (64, 128, 255); // Coral
+ yield return ColorBgra.FromBgr (128, 0, 255); // Deep pink
+ yield return ColorBgra.FromBgr (128, 64, 0); // Dark cerulean
+ yield return ColorBgra.FromBgr (128, 255, 0); // Spring green
+ yield return ColorBgra.FromBgr (128, 255, 255); // Light yellow
+ yield return ColorBgra.FromBgr (192, 192, 192); // Light gray
+ yield return ColorBgra.FromBgr (255, 0, 128); // Electric indigo
+ yield return ColorBgra.FromBgr (255, 128, 0); // Dodger blue
+ yield return ColorBgra.FromBgr (255, 128, 128); // Light slate blue
+ yield return ColorBgra.FromBgr (255, 255, 128); // Electric blue
+ }
+
+ private static IEnumerable RgbColorCube (ImmutableArray values)
+ {
+ for (int r = 0; r < values.Length; r++)
+ for (int g = 0; g < values.Length; g++)
+ for (int b = 0; b < values.Length; b++)
+ yield return ColorBgra.FromBgr (
+ b: values[b],
+ g: values[g],
+ r: values[r]);
+ }
+
+ private static IEnumerable RgbColorCube (IEnumerable valueSequence)
+ => RgbColorCube (valueSequence.ToImmutableArray ());
+
+ private static IEnumerable RgbColorCube (byte levels, byte factor)
+ => RgbColorCube (Enumerable.Range (0, levels).Select (i => Utility.ClampToByte (i * factor)));
+ }
+}
diff --git a/tests/Pinta.Effects.Tests/Assets/dithering1.png b/tests/Pinta.Effects.Tests/Assets/dithering1.png
new file mode 100644
index 000000000..b90dab76b
Binary files /dev/null and b/tests/Pinta.Effects.Tests/Assets/dithering1.png differ
diff --git a/tests/Pinta.Effects.Tests/Assets/dithering2.png b/tests/Pinta.Effects.Tests/Assets/dithering2.png
new file mode 100644
index 000000000..6d998932e
Binary files /dev/null and b/tests/Pinta.Effects.Tests/Assets/dithering2.png differ
diff --git a/tests/Pinta.Effects.Tests/Assets/dithering3.png b/tests/Pinta.Effects.Tests/Assets/dithering3.png
new file mode 100644
index 000000000..e82958ea2
Binary files /dev/null and b/tests/Pinta.Effects.Tests/Assets/dithering3.png differ
diff --git a/tests/Pinta.Effects.Tests/EffectsTest.cs b/tests/Pinta.Effects.Tests/EffectsTest.cs
index 4b360a953..6b33f7859 100644
--- a/tests/Pinta.Effects.Tests/EffectsTest.cs
+++ b/tests/Pinta.Effects.Tests/EffectsTest.cs
@@ -54,6 +54,33 @@ public void Clouds1 ()
// TODO:
}
+ [Test]
+ public void Dithering1 ()
+ {
+ var effect = new DitheringEffect ();
+ effect.Data.PaletteChoice = PredefinedPalettes.OldWindows16;
+ effect.Data.ErrorDiffusionMethod = PredefinedDiffusionMatrices.FloydSteinberg;
+ Utilities.TestEffect (effect, "dithering1.png");
+ }
+
+ [Test]
+ public void Dithering2 ()
+ {
+ var effect = new DitheringEffect ();
+ effect.Data.PaletteChoice = PredefinedPalettes.BlackWhite;
+ effect.Data.ErrorDiffusionMethod = PredefinedDiffusionMatrices.FloydSteinberg;
+ Utilities.TestEffect (effect, "dithering2.png");
+ }
+
+ [Test]
+ public void Dithering3 ()
+ {
+ var effect = new DitheringEffect ();
+ effect.Data.PaletteChoice = PredefinedPalettes.OldWindows16;
+ effect.Data.ErrorDiffusionMethod = PredefinedDiffusionMatrices.Stucki;
+ Utilities.TestEffect (effect, "dithering3.png");
+ }
+
[Test]
public void EdgeDetect1 ()
{