// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; using SixLabors.ImageSharp.Tests.Memory; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests; public static class TestImageExtensions { /// /// TODO: Consider adding this private processor to the library /// /// The image processing context. public static void MakeOpaque(this IImageProcessingContext ctx) => ctx.ApplyProcessor(new MakeOpaqueProcessor()); public static void DebugSave( this Image image, ITestImageProvider provider, FormattableString testOutputDetails, string extension = "png", bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true, IImageEncoder encoder = null) => image.DebugSave( provider, (object)testOutputDetails, extension, appendPixelTypeToFileName, appendSourceFileOrDescription, encoder); /// /// Saves the image for debugging purpose. /// /// The image. /// The image provider. /// Details to be concatenated to the test output file, describing the parameters of the test. /// The extension. /// A boolean indicating whether to append the pixel type to the output file name. /// A boolean indicating whether to append to the test output file name. /// Custom encoder to use. /// The input image. public static Image DebugSave( this Image image, ITestImageProvider provider, object testOutputDetails = null, string extension = "png", bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true, IImageEncoder encoder = null) { if (TestEnvironment.RunsWithCodeCoverage) { return image; } provider.Utility.SaveTestOutputFile( image, extension, testOutputDetails: testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName, appendSourceFileOrDescription: appendSourceFileOrDescription, encoder: encoder); return image; } public static void DebugSave( this Image image, ITestImageProvider provider, IImageEncoder encoder, FormattableString testOutputDetails, bool appendPixelTypeToFileName = true) => image.DebugSave(provider, encoder, (object)testOutputDetails, appendPixelTypeToFileName); /// /// Saves the image for debugging purpose. /// /// The image /// The image provider /// The image encoder /// Details to be concatenated to the test output file, describing the parameters of the test. /// A boolean indicating whether to append the pixel type to the output file name. public static void DebugSave( this Image image, ITestImageProvider provider, IImageEncoder encoder, object testOutputDetails = null, bool appendPixelTypeToFileName = true) => provider.Utility.SaveTestOutputFile( image, encoder: encoder, testOutputDetails: testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName); public static Image DebugSaveMultiFrame( this Image image, ITestImageProvider provider, object testOutputDetails = null, string extension = "png", bool appendPixelTypeToFileName = true) where TPixel : unmanaged, IPixel { if (TestEnvironment.RunsWithCodeCoverage) { return image; } provider.Utility.SaveTestOutputFileMultiFrame( image, extension, testOutputDetails: testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName); return image; } public static Image CompareToReferenceOutput( this Image image, ITestImageProvider provider, FormattableString testOutputDetails, string extension = "png", bool grayscale = false, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel => image.CompareToReferenceOutput( provider, (object)testOutputDetails, extension, grayscale, appendPixelTypeToFileName, appendSourceFileOrDescription); /// /// Compares the image against the expected Reference output, throws an exception if the images are not similar enough. /// The output file should be named identically to the output produced by . /// /// The pixel format. /// The image which should be compared to the reference image. /// The image provider. /// Details to be concatenated to the test output file, describing the parameters of the test. /// The extension /// A boolean indicating whether we should debug save + compare against a grayscale image, smaller in size. /// A boolean indicating whether to append the pixel type to the output file name. /// A boolean indicating whether to append to the test output file name. /// The image. public static Image CompareToReferenceOutput( this Image image, ITestImageProvider provider, object testOutputDetails = null, string extension = "png", bool grayscale = false, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel => CompareToReferenceOutput( image, ImageComparer.Tolerant(), provider, testOutputDetails, extension, grayscale, appendPixelTypeToFileName, appendSourceFileOrDescription); public static Image CompareToReferenceOutput( this Image image, ImageComparer comparer, ITestImageProvider provider, FormattableString testOutputDetails, string extension = "png", bool grayscale = false, bool appendPixelTypeToFileName = true) where TPixel : unmanaged, IPixel => image.CompareToReferenceOutput( comparer, provider, (object)testOutputDetails, extension, grayscale, appendPixelTypeToFileName); /// /// Compares the image against the expected Reference output, throws an exception if the images are not similar enough. /// The output file should be named identically to the output produced by . /// /// The pixel format. /// The image which should be compared to the reference output. /// The to use. /// The image provider. /// Details to be concatenated to the test output file, describing the parameters of the test. /// The extension /// A boolean indicating whether we should debug save + compare against a grayscale image, smaller in size. /// A boolean indicating whether to append the pixel type to the output file name. /// A boolean indicating whether to append to the test output file name. /// A custom decoder. /// The image. public static Image CompareToReferenceOutput( this Image image, ImageComparer comparer, ITestImageProvider provider, object testOutputDetails = null, string extension = "png", bool grayscale = false, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true, IImageDecoder decoder = null) where TPixel : unmanaged, IPixel { using (Image referenceImage = GetReferenceOutputImage( provider, testOutputDetails, extension, appendPixelTypeToFileName, appendSourceFileOrDescription, decoder)) { comparer.VerifySimilarity(referenceImage, image); } return image; } public static Image CompareFirstFrameToReferenceOutput( this Image image, ImageComparer comparer, ITestImageProvider provider, FormattableString testOutputDetails, string extension = "png", bool grayscale = false, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel => image.CompareFirstFrameToReferenceOutput( comparer, provider, (object)testOutputDetails, extension, grayscale, appendPixelTypeToFileName, appendSourceFileOrDescription); public static Image CompareFirstFrameToReferenceOutput( this Image image, ImageComparer comparer, ITestImageProvider provider, object testOutputDetails = null, string extension = "png", bool grayscale = false, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel { using (var firstFrameOnlyImage = new Image(image.Width, image.Height)) using (Image referenceImage = GetReferenceOutputImage( provider, testOutputDetails, extension, appendPixelTypeToFileName, appendSourceFileOrDescription)) { firstFrameOnlyImage.Frames.AddFrame(image.Frames.RootFrame); firstFrameOnlyImage.Frames.RemoveFrame(0); comparer.VerifySimilarity(referenceImage, firstFrameOnlyImage); } return image; } public static Image CompareToReferenceOutputMultiFrame( this Image image, ITestImageProvider provider, ImageComparer comparer, object testOutputDetails = null, string extension = "png", bool grayscale = false, bool appendPixelTypeToFileName = true) where TPixel : unmanaged, IPixel { using (Image referenceImage = GetReferenceOutputImageMultiFrame( provider, image.Frames.Count, testOutputDetails, extension, appendPixelTypeToFileName)) { comparer.VerifySimilarity(referenceImage, image); } return image; } public static Image GetReferenceOutputImage( this ITestImageProvider provider, object testOutputDetails = null, string extension = "png", bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true, IImageDecoder decoder = null) where TPixel : unmanaged, IPixel { string referenceOutputFile = provider.Utility.GetReferenceOutputFileName( extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription); if (!File.Exists(referenceOutputFile)) { throw new FileNotFoundException("Reference output file missing: " + referenceOutputFile, referenceOutputFile); } decoder ??= TestEnvironment.GetReferenceDecoder(referenceOutputFile); using FileStream stream = File.OpenRead(referenceOutputFile); return decoder.Decode(DecoderOptions.Default, stream); } public static Image GetReferenceOutputImageMultiFrame( this ITestImageProvider provider, int frameCount, object testOutputDetails = null, string extension = "png", bool appendPixelTypeToFileName = true) where TPixel : unmanaged, IPixel { string[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame( frameCount, extension, testOutputDetails, appendPixelTypeToFileName); var temporaryFrameImages = new List>(); IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]); foreach (string path in frameFiles) { if (!File.Exists(path)) { throw new FileNotFoundException("Reference output file missing: " + path); } using FileStream stream = File.OpenRead(path); Image tempImage = decoder.Decode(DecoderOptions.Default, stream); temporaryFrameImages.Add(tempImage); } Image firstTemp = temporaryFrameImages[0]; var result = new Image(firstTemp.Width, firstTemp.Height); foreach (Image fi in temporaryFrameImages) { result.Frames.AddFrame(fi.Frames.RootFrame); fi.Dispose(); } // Remove the initial empty frame: result.Frames.RemoveFrame(0); return result; } public static IEnumerable GetReferenceOutputSimilarityReports( this Image image, ITestImageProvider provider, ImageComparer comparer, object testOutputDetails = null, string extension = "png", bool appendPixelTypeToFileName = true) where TPixel : unmanaged, IPixel { using Image referenceImage = provider.GetReferenceOutputImage( testOutputDetails, extension, appendPixelTypeToFileName); return comparer.CompareImages(referenceImage, image); } public static Image ComparePixelBufferTo( this Image image, Span expectedPixels) where TPixel : unmanaged, IPixel { Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory actualPixels)); CompareBuffers(expectedPixels, actualPixels.Span); return image; } public static Image ComparePixelBufferTo( this Image image, Memory expectedPixels) where TPixel : unmanaged, IPixel => ComparePixelBufferTo(image, expectedPixels.Span); public static void CompareBuffers(Span expected, Span actual) where T : struct, IEquatable { Assert.True(expected.Length == actual.Length, "Buffer sizes are not equal!"); for (int i = 0; i < expected.Length; i++) { T x = expected[i]; T a = actual[i]; Assert.True(x.Equals(a), $"Buffers differ at position {i}! Expected: {x} | Actual: {a}"); } } public static void CompareBuffers(Buffer2D expected, Buffer2D actual) where T : struct, IEquatable { Assert.True(expected.Size() == actual.Size(), "Buffer sizes are not equal!"); for (int y = 0; y < expected.Height; y++) { Span expectedRow = expected.DangerousGetRowSpan(y); Span actualRow = actual.DangerousGetRowSpan(y); for (int x = 0; x < expectedRow.Length; x++) { T expectedVal = expectedRow[x]; T actualVal = actualRow[x]; Assert.True( expectedVal.Equals(actualVal), $"Buffers differ at position ({x},{y})! Expected: {expectedVal} | Actual: {actualVal}"); } } } /// /// All pixels in all frames should be exactly equal to 'expectedPixel'. /// /// The pixel type of the image. /// The image. public static Image ComparePixelBufferTo(this Image image, TPixel expectedPixel) where TPixel : unmanaged, IPixel { foreach (ImageFrame imageFrame in image.Frames) { imageFrame.ComparePixelBufferTo(expectedPixel); } return image; } /// /// All pixels in all frames should be exactly equal to 'expectedPixelColor.ToPixel()'. /// /// The pixel type of the image. /// The image. public static Image ComparePixelBufferTo(this Image image, Color expectedPixelColor) where TPixel : unmanaged, IPixel { foreach (ImageFrame imageFrame in image.Frames) { imageFrame.ComparePixelBufferTo(expectedPixelColor.ToPixel()); } return image; } /// /// All pixels in the frame should be exactly equal to 'expectedPixel'. /// /// The pixel type of the image. /// The image. public static ImageFrame ComparePixelBufferTo(this ImageFrame imageFrame, TPixel expectedPixel) where TPixel : unmanaged, IPixel { Assert.True(imageFrame.DangerousTryGetSinglePixelMemory(out Memory actualPixelMem)); Span actualPixels = actualPixelMem.Span; for (int i = 0; i < actualPixels.Length; i++) { Assert.True(expectedPixel.Equals(actualPixels[i]), $"Pixels are different on position {i}!"); } return imageFrame; } public static ImageFrame ComparePixelBufferTo( this ImageFrame image, Span expectedPixels) where TPixel : unmanaged, IPixel { Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory actualMem)); Span actual = actualMem.Span; Assert.True(expectedPixels.Length == actual.Length, "Buffer sizes are not equal!"); for (int i = 0; i < expectedPixels.Length; i++) { Assert.True(expectedPixels[i].Equals(actual[i]), $"Pixels are different on position {i}!"); } return image; } public static Image CompareToOriginal( this Image image, ITestImageProvider provider, IImageDecoder referenceDecoder = null) where TPixel : unmanaged, IPixel => CompareToOriginal(image, provider, ImageComparer.Tolerant(), referenceDecoder); public static Image CompareToOriginal( this Image image, ITestImageProvider provider, ImageComparer comparer, IImageDecoder referenceDecoder = null, DecoderOptions referenceDecoderOptions = null) where TPixel : unmanaged, IPixel { string path = TestImageProvider.GetFilePathOrNull(provider); if (path == null) { throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); } TestFile testFile = TestFile.Create(path); referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); using MemoryStream stream = new(testFile.Bytes); using Image original = referenceDecoder.Decode(referenceDecoderOptions ?? DecoderOptions.Default, stream); comparer.VerifySimilarity(original, image); return image; } public static Image CompareToOriginalMultiFrame( this Image image, ITestImageProvider provider, ImageComparer comparer, IImageDecoder referenceDecoder = null) where TPixel : unmanaged, IPixel { string path = TestImageProvider.GetFilePathOrNull(provider); if (path == null) { throw new InvalidOperationException("CompareToOriginal() works only with file providers!"); } TestFile testFile = TestFile.Create(path); referenceDecoder ??= TestEnvironment.GetReferenceDecoder(path); using MemoryStream stream = new(testFile.Bytes); using Image original = referenceDecoder.Decode(DecoderOptions.Default, stream); comparer.VerifySimilarity(original, image); return image; } /// /// Utility method for doing the following in one step: /// 1. Executing an operation (taken as a delegate) /// 2. Executing DebugSave() /// 3. Executing CompareToReferenceOutput() /// internal static void VerifyOperation( this TestImageProvider provider, ImageComparer comparer, Action> operation, FormattableString testOutputDetails, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); operation(image); image.DebugSave( provider, testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName, appendSourceFileOrDescription: appendSourceFileOrDescription); image.CompareToReferenceOutput( comparer, provider, testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName, appendSourceFileOrDescription: appendSourceFileOrDescription); } /// /// Utility method for doing the following in one step: /// 1. Executing an operation (taken as a delegate) /// 2. Executing DebugSave() /// 3. Executing CompareToReferenceOutput() /// internal static void VerifyOperation( this TestImageProvider provider, Action> operation, FormattableString testOutputDetails, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel => provider.VerifyOperation( ImageComparer.Tolerant(), operation, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription); /// /// Utility method for doing the following in one step: /// 1. Executing an operation (taken as a delegate) /// 2. Executing DebugSave() /// 3. Executing CompareToReferenceOutput() /// internal static void VerifyOperation( this TestImageProvider provider, ImageComparer comparer, Action> operation, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel => provider.VerifyOperation( comparer, operation, $"", appendPixelTypeToFileName, appendSourceFileOrDescription); /// /// Utility method for doing the following in one step: /// 1. Executing an operation (taken as a delegate) /// 2. Executing DebugSave() /// 3. Executing CompareToReferenceOutput() /// internal static void VerifyOperation( this TestImageProvider provider, Action> operation, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel => provider.VerifyOperation(operation, $"", appendPixelTypeToFileName, appendSourceFileOrDescription); /// /// Loads the expected image with a reference decoder + compares it to . /// Also performs a debug save using . /// /// The path to the encoded output file. internal static string VerifyEncoder( this Image image, ITestImageProvider provider, string extension, object testOutputDetails, IImageEncoder encoder, ImageComparer customComparer = null, bool appendPixelTypeToFileName = true, string referenceImageExtension = null, IImageDecoder referenceDecoder = null) where TPixel : unmanaged, IPixel { string actualOutputFile = provider.Utility.SaveTestOutputFile( image, extension, encoder, testOutputDetails, appendPixelTypeToFileName); referenceDecoder ??= TestEnvironment.GetReferenceDecoder(actualOutputFile); using FileStream stream = File.OpenRead(actualOutputFile); using Image encodedImage = referenceDecoder.Decode(DecoderOptions.Default, stream); ImageComparer comparer = customComparer ?? ImageComparer.Exact; comparer.VerifySimilarity(encodedImage, image); return actualOutputFile; } internal static AllocatorBufferCapacityConfigurator LimitAllocatorBufferCapacity( this TestImageProvider provider) where TPixel : unmanaged, IPixel { var allocator = new TestMemoryAllocator(); provider.Configuration.MemoryAllocator = allocator; return new AllocatorBufferCapacityConfigurator(allocator, Unsafe.SizeOf()); } internal static Image ToGrayscaleImage(this Buffer2D buffer, float scale) { var image = new Image(buffer.Width, buffer.Height); Assert.True(image.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory pixelMem)); Span pixels = pixelMem.Span; Span bufferSpan = buffer.DangerousGetSingleSpan(); for (int i = 0; i < bufferSpan.Length; i++) { float value = bufferSpan[i] * scale; var v = new Vector4(value, value, value, 1f); pixels[i].FromVector4(v); } return image; } private class MakeOpaqueProcessor : IImageProcessor { public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel => new MakeOpaqueProcessor(configuration, source, sourceRectangle); } private class MakeOpaqueProcessor : ImageProcessor where TPixel : unmanaged, IPixel { public MakeOpaqueProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) { } protected override void OnFrameApply(ImageFrame source) { Rectangle sourceRectangle = this.SourceRectangle; Configuration configuration = this.Configuration; var operation = new RowOperation(configuration, sourceRectangle, source.PixelBuffer); ParallelRowIterator.IterateRowIntervals( configuration, sourceRectangle, in operation); } private readonly struct RowOperation : IRowIntervalOperation { private readonly Configuration configuration; private readonly Rectangle bounds; private readonly Buffer2D source; public RowOperation(Configuration configuration, Rectangle bounds, Buffer2D source) { this.configuration = configuration; this.bounds = bounds; this.source = source; } public int GetRequiredBufferLength(Rectangle bounds) => bounds.Width; public void Invoke(in RowInterval rows, Span span) { for (int y = rows.Min; y < rows.Max; y++) { Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.bounds.Left, this.bounds.Width); PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, PixelConversionModifiers.Scale); for (int i = 0; i < span.Length; i++) { ref Vector4 v = ref span[i]; v.W = 1F; } PixelOperations.Instance.FromVector4Destructive(this.configuration, span, rowSpan, PixelConversionModifiers.Scale); } } } } } internal class AllocatorBufferCapacityConfigurator { private readonly TestMemoryAllocator allocator; private readonly int pixelSizeInBytes; public AllocatorBufferCapacityConfigurator(TestMemoryAllocator allocator, int pixelSizeInBytes) { this.allocator = allocator; this.pixelSizeInBytes = pixelSizeInBytes; } public void InBytes(int totalBytes) => this.allocator.BufferCapacityInBytes = totalBytes; public void InPixels(int totalPixels) => this.InBytes(totalPixels * this.pixelSizeInBytes); /// /// Set the maximum buffer capacity to bytesSqrt^2 bytes. /// public void InBytesSqrt(int bytesSqrt) => this.InBytes(bytesSqrt * bytesSqrt); /// /// Set the maximum buffer capacity to pixelsSqrt^2 x sizeof(TPixel) bytes. /// public void InPixelsSqrt(int pixelsSqrt) => this.InPixels(pixelsSqrt * pixelsSqrt); }