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 () {