diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index edde7dc893..9fbd0b5adb 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.IO; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; @@ -305,23 +306,46 @@ private void Write16Bit(Stream stream, Buffer2D pixels) private void Write8Bit(Stream stream, ImageFrame image) where TPixel : struct, IPixel { + bool isGray8 = typeof(TPixel) == typeof(Gray8); using (IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize8Bit, AllocationOptions.Clean)) - using (IQuantizedFrame quantized = this.quantizer.CreateFrameQuantizer(this.configuration, 256).QuantizeFrame(image)) { Span colorPalette = colorPaletteBuffer.GetSpan(); - int idx = 0; + if (isGray8) + { + this.Write8BitGray(stream, image, colorPalette); + } + else + { + this.Write8BitColor(stream, image, colorPalette); + } + } + } + + /// + /// Writes an 8 Bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The to write to. + /// The containing pixel data. + /// A byte span of size 1024 for the color palette. + private void Write8BitColor(Stream stream, ImageFrame image, Span colorPalette) + where TPixel : struct, IPixel + { + using (IQuantizedFrame quantized = this.quantizer.CreateFrameQuantizer(this.configuration, 256).QuantizeFrame(image)) + { + ReadOnlySpan quantizedColors = quantized.Palette.Span; var color = default(Rgba32); - ReadOnlySpan paletteSpan = quantized.Palette.Span; // TODO: Use bulk conversion here for better perf - foreach (TPixel quantizedColor in paletteSpan) + int idx = 0; + foreach (TPixel quantizedColor in quantizedColors) { quantizedColor.ToRgba32(ref color); colorPalette[idx] = color.B; colorPalette[idx + 1] = color.G; colorPalette[idx + 2] = color.R; - // Padding byte, always 0 + // Padding byte, always 0. colorPalette[idx + 3] = 0; idx += 4; } @@ -340,5 +364,43 @@ private void Write8Bit(Stream stream, ImageFrame image) } } } + + /// + /// Writes an 8 Bit gray image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The to write to. + /// The containing pixel data. + /// A byte span of size 1024 for the color palette. + private void Write8BitGray(Stream stream, ImageFrame image, Span colorPalette) + where TPixel : struct, IPixel + { + // Create a color palette with 256 different gray values. + for (int i = 0; i <= 255; i++) + { + int idx = i * 4; + byte grayValue = (byte)i; + colorPalette[idx] = grayValue; + colorPalette[idx + 1] = grayValue; + colorPalette[idx + 2] = grayValue; + + // Padding byte, always 0. + colorPalette[idx + 3] = 0; + } + + stream.Write(colorPalette); + + for (int y = image.Height - 1; y >= 0; y--) + { + ReadOnlySpan inputPixelRow = image.GetPixelRowSpan(y); + ReadOnlySpan outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow); + stream.Write(outputPixelRow); + + for (int i = 0; i < this.padding; i++) + { + stream.WriteByte(0); + } + } + } } } diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs index 52821cf478..b2d499c7fa 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs @@ -32,7 +32,7 @@ public class DrawImageTests }; [Theory] - [WithFile( TestImages.Png.Rainbow,nameof(BlendingModes), PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Rainbow, nameof(BlendingModes), PixelTypes.Rgba32)] public void ImageBlendingMatchesSvgSpecExamples(TestImageProvider provider, PixelColorBlendingMode mode) where TPixel : struct, IPixel { @@ -54,13 +54,13 @@ public void ImageBlendingMatchesSvgSpecExamples(TestImageProvider( using (Image image = provider.GetImage()) using (var blend = Image.Load(TestFile.Create(brushImage).Bytes)) { - Size size = new Size(image.Width * 3 / 4, image.Height *3/ 4); + Size size = new Size(image.Width * 3 / 4, image.Height * 3 / 4); Point position = new Point(image.Width / 8, image.Height / 8); blend.Mutate(x => x.Resize(size.Width, size.Height, KnownResamplers.Bicubic)); image.Mutate(x => x.DrawImage(blend, position, mode, opacity)); @@ -89,7 +89,7 @@ public void WorksWithDifferentConfigurations( { encoder.BitDepth = PngBitDepth.Bit16; } - + image.DebugSave(provider, testInfo, encoder: encoder); image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.01f), provider, @@ -138,7 +138,7 @@ public void WorksWithDifferentLocations(TestImageProvider provider, int testOutputDetails: $"{x}_{y}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - + background.CompareToReferenceOutput( provider, testOutputDetails: $"{x}_{y}", @@ -146,7 +146,7 @@ public void WorksWithDifferentLocations(TestImageProvider provider, int appendSourceFileOrDescription: false); } } - + [Theory] [WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)] public void DrawTransformed(TestImageProvider provider) @@ -166,12 +166,12 @@ public void DrawTransformed(TestImageProvider provider) // Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2); - image.Mutate(x => x.DrawImage(blend, position, .75F)); - + image.Mutate(x => x.DrawImage(blend, position, .75F)); + image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.002f), provider, - appendSourceFileOrDescription: false, + appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); } } @@ -197,6 +197,6 @@ void Test() } } - + } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index dd76e9443c..178e652ae7 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -3,10 +3,12 @@ using System.IO; +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 Xunit; @@ -169,8 +171,7 @@ public void Encode_8BitGray_WithV3Header_Works(TestImageProvider TestBmpEncoderCore( provider, bitsPerPixel, - supportTransparency: false, - ImageComparer.TolerantPercentage(0.01f)); + supportTransparency: false); [Theory] [WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)] @@ -179,8 +180,59 @@ public void Encode_8BitGray_WithV4Header_Works(TestImageProvider TestBmpEncoderCore( provider, bitsPerPixel, - supportTransparency: true, - ImageComparer.TolerantPercentage(0.01f)); + supportTransparency: true); + + [Theory] + [WithFile(Bit32Rgb, PixelTypes.Rgba32)] + public void Encode_8BitColor_WithWuQuantizer(TestImageProvider provider) + where TPixel : struct, IPixel + { + if (!TestEnvironment.Is64BitProcess) + { + return; + } + + using (Image image = provider.GetImage()) + { + var encoder = new BmpEncoder + { + BitsPerPixel = BmpBitsPerPixel.Pixel8, + Quantizer = new WuQuantizer(256) + }; + string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); + IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); + using (var referenceImage = Image.Load(actualOutputFile, referenceDecoder)) + { + referenceImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.01f), provider, extension: "bmp", appendPixelTypeToFileName: false); + } + } + } + + [Theory] + [WithFile(Bit32Rgb, PixelTypes.Rgba32)] + public void Encode_8BitColor_WithOctreeQuantizer(TestImageProvider provider) + where TPixel : struct, IPixel + { + if (!TestEnvironment.Is64BitProcess) + { + return; + } + + using (Image image = provider.GetImage()) + { + var encoder = new BmpEncoder + { + BitsPerPixel = BmpBitsPerPixel.Pixel8, + Quantizer = new OctreeQuantizer(256) + }; + string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); + IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); + using (var referenceImage = Image.Load(actualOutputFile, referenceDecoder)) + { + referenceImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.01f), provider, extension: "bmp", appendPixelTypeToFileName: false); + } + } + } [Theory] [WithFile(TestImages.Png.GrayAlpha2BitInterlaced, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] diff --git a/tests/Images/External b/tests/Images/External index 82f082ed3b..42b3b980ed 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 82f082ed3b47cc21b38dda9c5f8dda06dca5233a +Subproject commit 42b3b980ed07afd7b6603a5bfa6ffb91d6c8a124