// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using static SixLabors.ImageSharp.Tests.TestImages.Bmp; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Formats.Bmp; [Trait("Format", "Bmp")] public class BmpEncoderTests { private static BmpEncoder BmpEncoder => new(); public static readonly TheoryData BitsPerPixel = new() { BmpBitsPerPixel.Pixel24, BmpBitsPerPixel.Pixel32 }; public static readonly TheoryData RatioFiles = new() { { Car, 3780, 3780, PixelResolutionUnit.PixelsPerMeter }, { V5Header, 3780, 3780, PixelResolutionUnit.PixelsPerMeter }, { RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } }; public static readonly TheoryData BmpBitsPerPixelFiles = new() { { Bit1, BmpBitsPerPixel.Pixel1 }, { Bit2, BmpBitsPerPixel.Pixel2 }, { Bit4, BmpBitsPerPixel.Pixel4 }, { Bit8, BmpBitsPerPixel.Pixel8 }, { Rgb16, BmpBitsPerPixel.Pixel16 }, { Car, BmpBitsPerPixel.Pixel24 }, { Bit32Rgb, BmpBitsPerPixel.Pixel32 } }; [Theory] [MemberData(nameof(RatioFiles))] public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) { var testFile = TestFile.Create(imagePath); using Image input = testFile.CreateRgba32Image(); using var memStream = new MemoryStream(); input.Save(memStream, BmpEncoder); memStream.Position = 0; using var output = Image.Load(memStream); ImageMetadata meta = output.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); Assert.Equal(yResolution, meta.VerticalResolution); Assert.Equal(resolutionUnit, meta.ResolutionUnits); } [Theory] [MemberData(nameof(BmpBitsPerPixelFiles))] public void Encode_PreserveBitsPerPixel(string imagePath, BmpBitsPerPixel bmpBitsPerPixel) { var testFile = TestFile.Create(imagePath); using Image input = testFile.CreateRgba32Image(); using var memStream = new MemoryStream(); input.Save(memStream, BmpEncoder); memStream.Position = 0; using var output = Image.Load(memStream); BmpMetadata meta = output.Metadata.GetBmpMetadata(); Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); } [Theory] [WithTestPatternImages(nameof(BitsPerPixel), 24, 24, PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24)] public void Encode_IsNotBoundToSinglePixelType(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel); [Theory] [WithTestPatternImages(nameof(BitsPerPixel), 48, 24, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel), 47, 8, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel), 49, 7, PixelTypes.Rgba32)] [WithSolidFilledImages(nameof(BitsPerPixel), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel), 7, 5, PixelTypes.Rgba32)] public void Encode_WorksWithDifferentSizes(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel); [Theory] [WithFile(Bit32Rgb, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] [WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] [WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] [WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] public void Encode_32Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) // If supportTransparency is false, a v3 bitmap header will be written. where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] [WithFile(Bit32Rgb, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] [WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] [WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] [WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] public void Encode_32Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] // WinBmpv3 is a 24 bits per pixel image. [WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] public void Encode_24Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] [WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] public void Encode_24Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] public void Encode_16Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] public void Encode_16Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] [WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] public void Encode_8Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] [WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] public void Encode_8Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)] public void Encode_8BitGray_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore( provider, bitsPerPixel, supportTransparency: false); [Theory] [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)] public void Encode_4Bit_WithV3Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel { // Oddly the difference only happens locally but we'll not test for that. // I suspect the issue is with the reference codec. ImageComparer comparer = TestEnvironment.IsFramework ? ImageComparer.TolerantPercentage(0.0161F) : ImageComparer.Exact; TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false, customComparer: comparer); } [Theory] [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)] public void Encode_4Bit_WithV4Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel { // Oddly the difference only happens locally but we'll not test for that. // I suspect the issue is with the reference codec. ImageComparer comparer = TestEnvironment.IsFramework ? ImageComparer.TolerantPercentage(0.0161F) : ImageComparer.Exact; TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true, customComparer: comparer); } [Theory] [WithFile(Bit2, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel2)] public void Encode_2Bit_WithV3Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel { // arrange var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel }; using var memoryStream = new MemoryStream(); using Image input = provider.GetImage(BmpDecoder.Instance); // act encoder.Encode(input, memoryStream); memoryStream.Position = 0; // assert using var actual = Image.Load(memoryStream); ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual); Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image"); } [Theory] [WithFile(Bit2, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel2)] public void Encode_2Bit_WithV4Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel { // arrange var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel }; using var memoryStream = new MemoryStream(); using Image input = provider.GetImage(BmpDecoder.Instance); // act encoder.Encode(input, memoryStream); memoryStream.Position = 0; // assert using var actual = Image.Load(memoryStream); ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual); Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image"); } [Theory] [WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)] public void Encode_1Bit_WithV3Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] [WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)] public void Encode_1Bit_WithV4Header_Works( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)] public void Encode_8BitGray_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore( provider, bitsPerPixel, supportTransparency: true); [Theory] [WithFile(Bit32Rgb, PixelTypes.Rgba32)] public void Encode_8BitColor_WithWuQuantizer(TestImageProvider provider) where TPixel : unmanaged, IPixel { if (!TestEnvironment.Is64BitProcess) { return; } using Image image = provider.GetImage(); var encoder = new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel8, Quantizer = new WuQuantizer() }; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); // Use the default decoder to test our encoded image. This verifies the content. // We do not verify the reference image though as some are invalid. IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); using FileStream stream = File.OpenRead(actualOutputFile); using Image referenceImage = referenceDecoder.Decode(DecoderOptions.Default, stream); referenceImage.CompareToReferenceOutput( ImageComparer.TolerantPercentage(0.01f), provider, extension: "bmp", appendPixelTypeToFileName: false, decoder: new MagickReferenceDecoder(false)); } [Theory] [WithFile(Bit32Rgb, PixelTypes.Rgba32)] public void Encode_8BitColor_WithOctreeQuantizer(TestImageProvider provider) where TPixel : unmanaged, IPixel { if (!TestEnvironment.Is64BitProcess) { return; } using Image image = provider.GetImage(); var encoder = new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel8, Quantizer = new OctreeQuantizer() }; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); // Use the default decoder to test our encoded image. This verifies the content. // We do not verify the reference image though as some are invalid. IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); using FileStream stream = File.OpenRead(actualOutputFile); using Image referenceImage = referenceDecoder.Decode(DecoderOptions.Default, stream); referenceImage.CompareToReferenceOutput( ImageComparer.TolerantPercentage(0.01f), provider, extension: "bmp", appendPixelTypeToFileName: false, decoder: new MagickReferenceDecoder(false)); } [Theory] [WithFile(TestImages.Png.GrayAlpha2BitInterlaced, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] [WithFile(Bit32Rgba, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] public void Encode_PreservesAlpha(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] [WithFile(IccProfile, PixelTypes.Rgba32)] public void Encode_PreservesColorProfile(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image input = provider.GetImage(BmpDecoder.Instance, new()); ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; byte[] expectedProfileBytes = expectedProfile.ToByteArray(); using var memStream = new MemoryStream(); input.Save(memStream, new BmpEncoder()); memStream.Position = 0; using var output = Image.Load(memStream); ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile; byte[] actualProfileBytes = actualProfile.ToByteArray(); Assert.NotNull(actualProfile); Assert.Equal(expectedProfileBytes, actualProfileBytes); } [Theory] [InlineData(1, 66535)] [InlineData(66535, 1)] public void Encode_WorksWithSizeGreaterThen65k(int width, int height) { Exception exception = Record.Exception(() => { using Image image = new Image(width, height); using var memStream = new MemoryStream(); image.Save(memStream, BmpEncoder); }); Assert.Null(exception); } [Theory] [WithFile(Car, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] [WithFile(V5Header, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] public void Encode_WorksWithDiscontiguousBuffers(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel { provider.LimitAllocatorBufferCapacity().InBytesSqrt(100); TestBmpEncoderCore(provider, bitsPerPixel); } private static void TestBmpEncoderCore( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel, bool supportTransparency = true, // if set to true, will write a V4 header, otherwise a V3 header. IQuantizer quantizer = null, ImageComparer customComparer = null, IImageDecoder referenceDecoder = null) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); // There is no alpha in bmp with less then 32 bits per pixels, so the reference image will be made opaque. if (bitsPerPixel != BmpBitsPerPixel.Pixel32) { image.Mutate(c => c.MakeOpaque()); } var encoder = new BmpEncoder { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency, Quantizer = quantizer ?? KnownQuantizers.Octree }; // Does DebugSave & load reference CompareToReferenceInput(): image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder, customComparer); } }