From 682f5b73e3e8c98ec5a860903380a8afa8893f72 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 30 Jul 2016 21:20:13 +1000 Subject: [PATCH] Add format tests Former-commit-id: 05ae692a75c4b11f2e4474f84412f432f93e8753 Former-commit-id: 782629c7d712f5496c6ff4cdeea242eef0896e31 Former-commit-id: da773565a12c5de9186d546542b2b8f976903567 --- .../Quantizers/Options/Quantization.cs | 28 +++ .../Quantizers/Palette/PaletteQuantizer.cs | 2 +- src/ImageProcessorCore/Quantizers/Quantize.cs | 65 ++++++ .../Quantizers/QuantizedImage.cs | 3 +- src/ImageProcessorCore/Samplers/Crop.cs | 2 +- .../Samplers/EntropyCrop.cs | 2 +- src/ImageProcessorCore/Samplers/Pad.cs | 2 +- src/ImageProcessorCore/Samplers/Resize.cs | 2 +- .../Formats/Bmp/BitmapTests.cs | 47 +++++ .../Formats/GeneralFormatTests.cs | 199 ++++++++++++++++++ .../GeneralFormatTests.cs => Png/PngTests.cs} | 22 +- 11 files changed, 356 insertions(+), 18 deletions(-) create mode 100644 src/ImageProcessorCore/Quantizers/Options/Quantization.cs create mode 100644 src/ImageProcessorCore/Quantizers/Quantize.cs create mode 100644 tests/ImageProcessorCore.Tests/Formats/Bmp/BitmapTests.cs create mode 100644 tests/ImageProcessorCore.Tests/Formats/GeneralFormatTests.cs rename tests/ImageProcessorCore.Tests/Formats/{Jpg/GeneralFormatTests.cs => Png/PngTests.cs} (52%) diff --git a/src/ImageProcessorCore/Quantizers/Options/Quantization.cs b/src/ImageProcessorCore/Quantizers/Options/Quantization.cs new file mode 100644 index 0000000000..3dd0c87997 --- /dev/null +++ b/src/ImageProcessorCore/Quantizers/Options/Quantization.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + /// + /// Provides enumeration over how an image should be quantized. + /// + public enum Quantization + { + /// + /// An adaptive Octree quantizer. Fast with good quality. + /// + Octree, + + /// + /// Xiaolin Wu's Color Quantizer which generates high quality output. + /// + Wu, + + /// + /// Palette based, Uses the collection of web-safe colors by default. + /// + Palette + } +} diff --git a/src/ImageProcessorCore/Quantizers/Palette/PaletteQuantizer.cs b/src/ImageProcessorCore/Quantizers/Palette/PaletteQuantizer.cs index 6fafad5f09..9de4a4dc4a 100644 --- a/src/ImageProcessorCore/Quantizers/Palette/PaletteQuantizer.cs +++ b/src/ImageProcessorCore/Quantizers/Palette/PaletteQuantizer.cs @@ -101,7 +101,7 @@ namespace ImageProcessorCore.Quantizers int leastDistance = int.MaxValue; int red = bytes[0]; int green = bytes[1]; - int blue = bytes[3]; + int blue = bytes[2]; // Loop through the entire palette, looking for the closest color match for (int index = 0; index < this.colors.Length; index++) diff --git a/src/ImageProcessorCore/Quantizers/Quantize.cs b/src/ImageProcessorCore/Quantizers/Quantize.cs new file mode 100644 index 0000000000..2db63a76b8 --- /dev/null +++ b/src/ImageProcessorCore/Quantizers/Quantize.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +using ImageProcessorCore.Quantizers; + +namespace ImageProcessorCore +{ + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Applies quantization to the image. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The quantization mode to apply to perform the operation. + /// The maximum number of colors to return. Defaults to 256. + /// The . + public static Image Quantize(this Image source, Quantization mode = Quantization.Octree, int maxColors = 256) + where T : IPackedVector + where TP : struct + { + IQuantizer quantizer; + switch (mode) + { + case Quantization.Wu: + quantizer = new WuQuantizer(); + break; + + case Quantization.Palette: + quantizer = new PaletteQuantizer(); + break; + + default: + quantizer = new OctreeQuantizer(); + break; + } + + return Quantize(source, quantizer, maxColors); + } + + /// + /// Applies quantization to the image. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The quantizer to apply to perform the operation. + /// The maximum number of colors to return. + /// The . + public static Image Quantize(this Image source, IQuantizer quantizer, int maxColors) + where T : IPackedVector + where TP : struct + { + QuantizedImage quantizedImage = quantizer.Quantize(source, maxColors); + source.SetPixels(source.Width, source.Height, quantizedImage.ToImage().Pixels); + return source; + } + } +} diff --git a/src/ImageProcessorCore/Quantizers/QuantizedImage.cs b/src/ImageProcessorCore/Quantizers/QuantizedImage.cs index 123efe6533..680f8b7ab0 100644 --- a/src/ImageProcessorCore/Quantizers/QuantizedImage.cs +++ b/src/ImageProcessorCore/Quantizers/QuantizedImage.cs @@ -90,9 +90,8 @@ namespace ImageProcessorCore.Quantizers Bootstrapper.Instance.ParallelOptions, i => { - int offset = i * 4; T color = this.Palette[Math.Min(palletCount, this.Pixels[i])]; - pixels[offset] = color; + pixels[i] = color; }); image.SetPixels(this.Width, this.Height, pixels); diff --git a/src/ImageProcessorCore/Samplers/Crop.cs b/src/ImageProcessorCore/Samplers/Crop.cs index 04c96dbd1a..84d343d6d6 100644 --- a/src/ImageProcessorCore/Samplers/Crop.cs +++ b/src/ImageProcessorCore/Samplers/Crop.cs @@ -1,7 +1,7 @@ // // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. -// ------------------------------------------------------------------------------------------------------------------- +// namespace ImageProcessorCore { diff --git a/src/ImageProcessorCore/Samplers/EntropyCrop.cs b/src/ImageProcessorCore/Samplers/EntropyCrop.cs index 631662d800..cbc9eedee8 100644 --- a/src/ImageProcessorCore/Samplers/EntropyCrop.cs +++ b/src/ImageProcessorCore/Samplers/EntropyCrop.cs @@ -1,7 +1,7 @@ // // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. -// ------------------------------------------------------------------------------------------------------------------- +// namespace ImageProcessorCore { diff --git a/src/ImageProcessorCore/Samplers/Pad.cs b/src/ImageProcessorCore/Samplers/Pad.cs index 0a7277144e..6be9f654c4 100644 --- a/src/ImageProcessorCore/Samplers/Pad.cs +++ b/src/ImageProcessorCore/Samplers/Pad.cs @@ -1,7 +1,7 @@ // // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. -// ------------------------------------------------------------------------------------------------------------------- +// namespace ImageProcessorCore { diff --git a/src/ImageProcessorCore/Samplers/Resize.cs b/src/ImageProcessorCore/Samplers/Resize.cs index e9d3093f4d..40b07469ec 100644 --- a/src/ImageProcessorCore/Samplers/Resize.cs +++ b/src/ImageProcessorCore/Samplers/Resize.cs @@ -1,7 +1,7 @@ // // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. -// ------------------------------------------------------------------------------------------------------------------- +// namespace ImageProcessorCore { diff --git a/tests/ImageProcessorCore.Tests/Formats/Bmp/BitmapTests.cs b/tests/ImageProcessorCore.Tests/Formats/Bmp/BitmapTests.cs new file mode 100644 index 0000000000..23884e9ed6 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/Formats/Bmp/BitmapTests.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + using System.IO; + + using Formats; + + using Xunit; + + public class BitmapTests : FileTestBase + { + [Fact] + public void BitmapCanEncodeDifferentBitRates() + { + const string path = "TestOutput/Bmp"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Image image = new Image(stream); + string encodeFilename = path + "/24-" + Path.GetFileNameWithoutExtension(file) + ".bmp"; + + using (FileStream output = File.OpenWrite(encodeFilename)) + { + image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel24 }); + } + + encodeFilename = path + "/32-" + Path.GetFileNameWithoutExtension(file) + ".bmp"; + + using (FileStream output = File.OpenWrite(encodeFilename)) + { + image.Save(output, new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32 }); + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/Formats/GeneralFormatTests.cs b/tests/ImageProcessorCore.Tests/Formats/GeneralFormatTests.cs new file mode 100644 index 0000000000..462a39cebc --- /dev/null +++ b/tests/ImageProcessorCore.Tests/Formats/GeneralFormatTests.cs @@ -0,0 +1,199 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + using System; + using System.IO; + + using Xunit; + + public class GeneralFormatTests : FileTestBase + { + [Fact] + public void ResolutionShouldChange() + { + const string path = "TestOutput/Resolution"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + string filename = Path.GetFileName(file); + + Image image = new Image(stream); + using (FileStream output = File.OpenWrite($"{path}/{filename}")) + { + image.VerticalResolution = 150; + image.HorizontalResolution = 150; + image.Save(output); + } + } + } + } + + [Fact] + public void ImageCanEncodeToString() + { + const string path = "TestOutput/ToString"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Image image = new Image(stream); + string filename = path + "/" + Path.GetFileNameWithoutExtension(file) + ".txt"; + File.WriteAllText(filename, image.ToString()); + } + } + } + + [Fact] + public void DecodeThenEncodeImageFromStreamShouldSucceed() + { + const string path = "TestOutput/Encode"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Image image = new Image(stream); + string encodeFilename = path + "/" + Path.GetFileName(file); + + using (FileStream output = File.OpenWrite(encodeFilename)) + { + image.Save(output); + } + } + } + } + + [Fact] + public void QuantizeImageShouldPreserveMaximumColorPrecision() + { + const string path = "TestOutput/Quantize"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Image image = new Image(stream); + + // Copy the original pixels to save decoding time. + Color[] pixels = new Color[image.Width * image.Height]; + Array.Copy(image.Pixels, pixels, image.Pixels.Length); + + using (FileStream output = File.OpenWrite($"{path}/Octree-{Path.GetFileName(file)}")) + { + image.Quantize(Quantization.Octree) + .Save(output, image.CurrentImageFormat); + + } + + image.SetPixels(image.Width, image.Height, pixels); + using (FileStream output = File.OpenWrite($"{path}/Wu-{Path.GetFileName(file)}")) + { + image.Quantize(Quantization.Wu) + .Save(output, image.CurrentImageFormat); + } + + image.SetPixels(image.Width, image.Height, pixels); + using (FileStream output = File.OpenWrite($"{path}/Palette-{Path.GetFileName(file)}")) + { + image.Quantize(Quantization.Palette) + .Save(output, image.CurrentImageFormat); + } + } + } + } + + [Fact] + public void ImageCanConvertFormat() + { + const string path = "TestOutput/Format"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Image image = new Image(stream); + using (FileStream output = File.OpenWrite($"{path}/{Path.GetFileNameWithoutExtension(file)}.gif")) + { + image.SaveAsGif(output); + } + + using (FileStream output = File.OpenWrite($"{path}/{Path.GetFileNameWithoutExtension(file)}.bmp")) + { + image.SaveAsBmp(output); + } + + using (FileStream output = File.OpenWrite($"{path}/{Path.GetFileNameWithoutExtension(file)}.jpg")) + { + image.SaveAsJpeg(output); + } + + using (FileStream output = File.OpenWrite($"{path}/{Path.GetFileNameWithoutExtension(file)}.png")) + { + image.SaveAsPng(output); + } + } + } + } + + [Fact] + public void ImageShouldPreservePixelByteOrderWhenSerialized() + { + const string path = "TestOutput/Serialized"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + Image image = new Image(stream); + byte[] serialized; + using (MemoryStream memoryStream = new MemoryStream()) + { + image.Save(memoryStream); + memoryStream.Flush(); + serialized = memoryStream.ToArray(); + } + + using (MemoryStream memoryStream = new MemoryStream(serialized)) + { + Image image2 = new Image(memoryStream); + using (FileStream output = File.OpenWrite($"{path}/{Path.GetFileName(file)}")) + { + image2.Save(output); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/Formats/Jpg/GeneralFormatTests.cs b/tests/ImageProcessorCore.Tests/Formats/Png/PngTests.cs similarity index 52% rename from tests/ImageProcessorCore.Tests/Formats/Jpg/GeneralFormatTests.cs rename to tests/ImageProcessorCore.Tests/Formats/Png/PngTests.cs index 69e2ef0f10..c86238cbac 100644 --- a/tests/ImageProcessorCore.Tests/Formats/Jpg/GeneralFormatTests.cs +++ b/tests/ImageProcessorCore.Tests/Formats/Png/PngTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // @@ -7,30 +7,30 @@ namespace ImageProcessorCore.Tests { using System.IO; + using Formats; + using Xunit; - public class GeneralFormatTests : FileTestBase + public class PngTests : FileTestBase { [Fact] - public void ResolutionShouldChange() + public void ImageCanSaveIndexedPng() { - if (!Directory.Exists("TestOutput/Resolution")) + const string path = "TestOutput/Png"; + if (!Directory.Exists(path)) { - Directory.CreateDirectory("TestOutput/Resolution"); + Directory.CreateDirectory(path); } foreach (string file in Files) { using (FileStream stream = File.OpenRead(file)) { - string filename = Path.GetFileName(file); - Image image = new Image(stream); - using (FileStream output = File.OpenWrite($"TestOutput/Resolution/{filename}")) + using (FileStream output = File.OpenWrite($"{path}/{Path.GetFileNameWithoutExtension(file)}.png")) { - image.VerticalResolution = 150; - image.HorizontalResolution = 150; - image.Save(output); + image.Quality = 256; + image.Save(output, new PngFormat()); } } }