From e6e97ed4041cf2b6ef1aa6920be7cc4af97868d7 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Fri, 24 Mar 2017 07:49:10 +0000 Subject: [PATCH] Add image comparer test helper Add a couple of png smoke tests that encode and then decode an image in png then does a visual compare to verify they render the same. --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 5 +- .../Formats/Png/PngInterlaceMode.cs | 4 +- .../Formats/Png/PngEncoderTests.cs | 64 +++++- .../Formats/Png/PngSmokeTests.cs | 62 ++++++ tests/ImageSharp.Tests/ImageComparer.cs | 84 ++++++++ .../WithTestPatternImageAttribute.cs | 38 ++++ .../ImageProviders/TestImageProvider.cs | 13 +- .../ImageProviders/TestPatternProvider.cs | 184 ++++++++++++++++++ .../TestUtilities/ImagingTestCaseUtility.cs | 34 +++- 9 files changed, 470 insertions(+), 18 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs create mode 100644 tests/ImageSharp.Tests/ImageComparer.cs create mode 100644 tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs create mode 100644 tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 7950d260c..e97eaed58 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -545,11 +545,10 @@ namespace ImageSharp.Formats /// The pixel format. /// The containing image data. /// The image base. - private void WritePhysicalChunk(Stream stream, ImageBase imageBase) + private void WritePhysicalChunk(Stream stream, Image image) where TColor : struct, IPixel { - Image image = imageBase as Image; - if (image != null && image.MetaData.HorizontalResolution > 0 && image.MetaData.VerticalResolution > 0) + if (image.MetaData.HorizontalResolution > 0 && image.MetaData.VerticalResolution > 0) { // 39.3700787 = inches in a meter. int dpmX = (int)Math.Round(image.MetaData.HorizontalResolution * 39.3700787D); diff --git a/src/ImageSharp/Formats/Png/PngInterlaceMode.cs b/src/ImageSharp/Formats/Png/PngInterlaceMode.cs index e32e808c1..ec3b8ebe7 100644 --- a/src/ImageSharp/Formats/Png/PngInterlaceMode.cs +++ b/src/ImageSharp/Formats/Png/PngInterlaceMode.cs @@ -13,11 +13,11 @@ namespace ImageSharp.Formats /// /// Non interlaced /// - None, + None = 0, /// /// Adam 7 interlacing. /// - Adam7 + Adam7 = 1 } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 49be75139..16906c1fa 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -9,12 +9,12 @@ namespace ImageSharp.Tests { using System.IO; using System.Threading.Tasks; - + using ImageSharp.IO; using Xunit; public class PngEncoderTests : FileTestBase { - [Fact] + [Fact(Skip ="Slow intergration test")] public void ImageCanSaveIndexedPng() { string path = CreateOutputDirectory("Png", "Indexed"); @@ -32,7 +32,7 @@ namespace ImageSharp.Tests } } - [Fact] + [Fact(Skip = "Slow intergration test")] public void ImageCanSavePngInParallel() { string path = this.CreateOutputDirectory("Png"); @@ -50,5 +50,63 @@ namespace ImageSharp.Tests } }); } + + [Theory] + [WithBlankImages(1, 1, PixelTypes.All)] + public void WritesFileMarker(TestImageProvider provider) + where TColor : struct, IPixel + { + using (Image image = provider.GetImage()) + using (EndianBinaryReader reader = Encode(image, null)) + { + + byte[] data = reader.ReadBytes(8); + byte[] expected = { + 0x89, // Set the high bit. + 0x50, // P + 0x4E, // N + 0x47, // G + 0x0D, // Line ending CRLF + 0x0A, // Line ending CRLF + 0x1A, // EOF + 0x0A // LF + }; + + Assert.Equal(expected, data); + } + } + + [Theory] + [WithBlankImages(1, 1, PixelTypes.All)] + [WithBlankImages(10, 10, PixelTypes.StandardImageClass)] + public void WritesFileHeaderHasHeightAndWidth(TestImageProvider provider) + where TColor : struct, IPixel + { + using (Image image = provider.GetImage()) + using (EndianBinaryReader reader = Encode(image, null)) + { + reader.ReadBytes(8); // throw away the file header + uint width = reader.ReadUInt32(); + uint height = reader.ReadUInt32(); + + byte bitDepth = reader.ReadByte(); + byte colorType = reader.ReadByte(); + byte compressionMethod = reader.ReadByte(); + byte filterMethod = reader.ReadByte(); + byte interlaceMethod = reader.ReadByte(); + + Assert.Equal(image.Width, (int)width); + Assert.Equal(image.Height, (int)height); + } + } + + private static EndianBinaryReader Encode(Image img, IEncoderOptions options) + where TColor : struct, IPixel + { + MemoryStream stream = new MemoryStream(); + new PngEncoder().Encode(img, stream, null); + stream.Position = 0; + return new EndianBinaryReader(Endianness.BigEndian, stream); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs new file mode 100644 index 000000000..7a15e30e9 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests.Formats.Png +{ + using System; + using System.Collections.Generic; + using System.Text; + using System.IO; + using Xunit; + using ImageSharp.Formats; + using System.Linq; + using ImageSharp.IO; + + public class PngSmokeTests + { + [Theory] + [WithTestPatternImages(300, 300, PixelTypes.All)] + public void WritesFileMarker(TestImageProvider provider) + where TColor : struct, IPixel + { + // does saving a file then repoening mean both files are identical??? + using (Image image = provider.GetImage()) + using (MemoryStream ms = new MemoryStream()) + { + image.Save(provider.Utility.GetTestOutputFileName("bmp")); + + image.Save(ms, new PngEncoder()); + ms.Position = 0; + using (Image img2 = new Image(ms, new Configuration(new PngFormat()))) + { + img2.Save(provider.Utility.GetTestOutputFileName("bmp", "_loaded"), new BmpEncoder()); + ImageComparer.VisualComparer(image, img2); + } + } + } + + [Theory] + [WithTestPatternImages(300, 300, PixelTypes.All)] + public void Resize(TestImageProvider provider) + where TColor : struct, IPixel + { + // does saving a file then repoening mean both files are identical??? + using (Image image = provider.GetImage()) + using (MemoryStream ms = new MemoryStream()) + { + // image.Save(provider.Utility.GetTestOutputFileName("png")); + image.Resize(100, 100); + // image.Save(provider.Utility.GetTestOutputFileName("png", "resize")); + + image.Save(ms, new PngEncoder()); + ms.Position = 0; + using (Image img2 = new Image(ms, new Configuration(new PngFormat()))) + { + ImageComparer.VisualComparer(image, img2); + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/ImageComparer.cs b/tests/ImageSharp.Tests/ImageComparer.cs new file mode 100644 index 000000000..091edd343 --- /dev/null +++ b/tests/ImageSharp.Tests/ImageComparer.cs @@ -0,0 +1,84 @@ +namespace ImageSharp.Tests +{ + using System; + using ImageSharp; + using Xunit; + + /// + /// Class to perform simple image comparisons. + /// + public static class ImageComparer + { + const int DefaultScalingFactor = 32; + const int DefaultSegmentThreshold = 3; + const float DefaultImageThreshold = 0.000f; + + public static void VisualComparer(Image expected, Image actual, float imageTheshold = DefaultImageThreshold, byte segmentThreshold = DefaultSegmentThreshold, int scalingFactor = DefaultScalingFactor) + where TColorA : struct, IPixel + where TColorB : struct, IPixel + { + var percentage = expected.PercentageDifference(actual, segmentThreshold, scalingFactor); + + Assert.InRange(percentage, 0, imageTheshold); + } + + public static float PercentageDifference(this Image source, Image target, byte segmentThreshold = DefaultSegmentThreshold, int scalingFactor = DefaultScalingFactor) + where TColorA : struct, IPixel + where TColorB : struct, IPixel + { + // code adapted from https://www.codeproject.com/Articles/374386/Simple-image-comparison-in-NET + Fast2DArray differences = GetDifferences(source, target, scalingFactor); + + int diffPixels = 0; + + foreach (byte b in differences.Data) + { + if (b > segmentThreshold) { diffPixels++; } + } + + return diffPixels / (scalingFactor * scalingFactor); + } + + private static Fast2DArray GetDifferences(Image source, Image target, int scalingFactor) + where TColorA : struct, IPixel + where TColorB : struct, IPixel + { + Fast2DArray differences = new Fast2DArray(scalingFactor, scalingFactor); + Fast2DArray firstGray = source.GetGrayScaleValues(scalingFactor); + Fast2DArray secondGray = target.GetGrayScaleValues(scalingFactor); + + for (int y = 0; y < scalingFactor; y++) + { + for (int x = 0; x < scalingFactor; x++) + { + differences[x, y] = (byte)Math.Abs(firstGray[x, y] - secondGray[x, y]); + } + } + + return differences; + } + + private static Fast2DArray GetGrayScaleValues(this Image source, int scalingFactor) + where TColorA : struct, IPixel + { + byte[] buffer = new byte[4]; + using (Image img = new Image(source).Resize(scalingFactor, scalingFactor).Grayscale()) + { + using (PixelAccessor pixels = img.Lock()) + { + Fast2DArray grayScale = new Fast2DArray(scalingFactor, scalingFactor); + for (int y = 0; y < scalingFactor; y++) + { + for (int x = 0; x < scalingFactor; x++) + { + pixels[x, y].ToXyzBytes(buffer, 0); + grayScale[x, y] = buffer[1]; + } + } + + return grayScale; + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs new file mode 100644 index 000000000..98bc45f5b --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Reflection; + + /// + /// Triggers passing instances which produce a blank image of size width * height. + /// One instance will be passed for each the pixel format defined by the pixelTypes parameter + /// + public class WithTestPatternImagesAttribute : ImageDataAttributeBase + { + /// + /// Triggers passing an that produces a test pattern image of size width * height + /// + /// The required width + /// The required height + /// The requested parameter + /// Additional theory parameter values + public WithTestPatternImagesAttribute(int width, int height, PixelTypes pixelTypes, params object[] additionalParameters) + : base(pixelTypes, additionalParameters) + { + this.Width = width; + this.Height = height; + } + + public int Width { get; } + public int Height { get; } + + protected override string GetFactoryMethodName(MethodInfo testMethod) => "TestPattern"; + + protected override object[] GetFactoryMethodArgs(MethodInfo testMethod, Type factoryType) => new object[] { this.Width, this.Height }; + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs index cdb31ab69..4ec0fb507 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs @@ -25,13 +25,20 @@ namespace ImageSharp.Tests public ImagingTestCaseUtility Utility { get; private set; } public GenericFactory Factory { get; private set; } = new GenericFactory(); - - public static TestImageProvider Blank( + + public static TestImageProvider TestPattern( int width, int height, MethodInfo testMethod = null, PixelTypes pixelTypeOverride = PixelTypes.Undefined) - => new BlankProvider(width, height).Init(testMethod, pixelTypeOverride); + => new TestPatternProvider(width, height).Init(testMethod, pixelTypeOverride); + + public static TestImageProvider Blank( + int width, + int height, + MethodInfo testMethod = null, + PixelTypes pixelTypeOverride = PixelTypes.Undefined) + => new BlankProvider(width, height).Init(testMethod, pixelTypeOverride); public static TestImageProvider File( string filePath, diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs new file mode 100644 index 000000000..8d5e77554 --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs @@ -0,0 +1,184 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.Collections.Generic; + using System.Numerics; + + public abstract partial class TestImageProvider + where TColor : struct, IPixel + { + private class TestPatternProvider : TestImageProvider + { + static Dictionary> testImages = new Dictionary>(); + + public TestPatternProvider(int width, int height) + { + this.Width = width; + this.Height = height; + } + + public override string SourceFileOrDescription => $"TestPattern{this.Width}x{this.Height}"; + + protected int Height { get; } + + protected int Width { get; } + + public override Image GetImage() + { + lock (testImages) + { + if (!testImages.ContainsKey(this.SourceFileOrDescription)) + { + var image = new Image(this.Width, this.Height); + DrawTestPattern(image); + testImages.Add(this.SourceFileOrDescription, image); + } + + return new Image(testImages[this.SourceFileOrDescription]); + } + } + + private static void DrawTestPattern(Image image) + { + // first lets split the image into 4 quadrants + using (var pixels = image.Lock()) + { + BlackWhiteChecker(pixels); // top left + HorizontalLines(pixels); // top right + TransparentGradients(pixels); // bottom left + Raninbow(pixels); // bottom right + } + } + + private static void HorizontalLines(PixelAccessor pixels) + { + // topLeft + int left = pixels.Width / 2; + int right = pixels.Width; + int top = 0; + int bottom = pixels.Height / 2; + int stride = pixels.Width / 12; + TColor[] c = { + NamedColors.HotPink, + NamedColors.Blue + }; + int p = 0; + for (var y = top; y < bottom; y++) + { + for (var x = left; x < right; x++) + { + if (x % stride == 0) + { + p++; + p = p % c.Length; + } + pixels[x, y] = c[p]; + } + } + } + + private static void BlackWhiteChecker(PixelAccessor pixels) + { + // topLeft + int left = 0; + int right = pixels.Width / 2; + int top = 0; + int bottom = pixels.Height / 2; + int stride = pixels.Width / 6; + TColor[] c = { + NamedColors.Black, + NamedColors.White + }; + + int p = 0; + for (var y = top; y < bottom; y++) + { + if (y % stride == 0) + { + p++; + p = p % c.Length; + } + var pstart = p; + for (var x = left; x < right; x++) + { + if (x % stride == 0) + { + p++; + p = p % c.Length; + } + pixels[x, y] = c[p]; + } + p = pstart; + } + } + + private static void TransparentGradients(PixelAccessor pixels) + { + // topLeft + int left = 0; + int right = pixels.Width / 2; + int top = pixels.Height / 2; + int bottom = pixels.Height; + int height = (int)Math.Ceiling(pixels.Height / 6f); + + Vector4 red = Color.Red.ToVector4(); // use real color so we can see har it translates in the test pattern + Vector4 green = Color.Green.ToVector4(); // use real color so we can see har it translates in the test pattern + Vector4 blue = Color.Blue.ToVector4(); // use real color so we can see har it translates in the test pattern + + TColor c = default(TColor); + + for (var x = left; x < right; x++) + { + blue.W = red.W = green.W = (float)x / (float)right; + + c.PackFromVector4(red); + var topBand = top; + for (var y = topBand; y < top + height; y++) + { + pixels[x, y] = c; + } + topBand = topBand + height; + c.PackFromVector4(green); + for (var y = topBand; y < topBand + height; y++) + { + pixels[x, y] = c; + } + topBand = topBand + height; + c.PackFromVector4(blue); + for (var y = topBand; y < bottom; y++) + { + pixels[x, y] = c; + } + } + + } + private static void Raninbow(PixelAccessor pixels) + { + int left = pixels.Width / 2; + int right = pixels.Width; + int top = pixels.Height / 2; + int bottom = pixels.Height; + + int pixelCount = left * top; + uint stepsPerPixel = (uint)(uint.MaxValue / pixelCount); + TColor c = default(TColor); + Color t = new Color(0); + uint inital = 0; + for (var x = left; x < right; x++) + for (var y = top; y < bottom; y++) + { + t.PackedValue += stepsPerPixel; + var v = t.ToVector4(); + //v.W = (x - left) / (float)left; + c.PackFromVector4(v); + pixels[x, y] = c; + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index bcccd1b44..1c960e0e8 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -44,13 +44,26 @@ namespace ImageSharp.Tests /// /// /// The required extension - public string GetTestOutputFileName(string extension = null) + public string GetTestOutputFileName(string extension = null, string tag = null) { string fn = string.Empty; + if (string.IsNullOrWhiteSpace(extension)) + { + extension = null; + } + fn = Path.GetFileNameWithoutExtension(this.SourceFileOrDescription); - extension = extension ?? Path.GetExtension(this.SourceFileOrDescription); - extension = extension ?? ".bmp"; + + if (string.IsNullOrWhiteSpace(extension)) + { + extension = Path.GetExtension(this.SourceFileOrDescription); + } + + if (string.IsNullOrWhiteSpace(extension)) + { + extension = ".bmp"; + } if (extension[0] != '.') { @@ -65,7 +78,14 @@ namespace ImageSharp.Tests pixName = '_' + pixName; } - return $"{this.GetTestOutputDir()}/{this.TestName}{pixName}{fn}{extension}"; + tag = tag ?? string.Empty; + if (tag != string.Empty) + { + tag= '_' + tag; + } + + + return $"{this.GetTestOutputDir()}/{this.TestName}{pixName}{fn}{tag}{extension}"; } /// @@ -80,7 +100,7 @@ namespace ImageSharp.Tests where TColor : struct, IPixel { string path = this.GetTestOutputFileName(extension); - + extension = Path.GetExtension(path); IImageFormat format = GetImageFormatByExtension(extension); encoder = encoder ?? format.Encoder; @@ -99,8 +119,8 @@ namespace ImageSharp.Tests private static IImageFormat GetImageFormatByExtension(string extension) { - extension = extension.ToLower(); - return Configuration.Default.ImageFormats.First(f => f.SupportedExtensions.Contains(extension)); + extension = extension?.TrimStart('.'); + return Configuration.Default.ImageFormats.First(f => f.SupportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)); } private string GetTestOutputDir()