diff --git a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs index 1b73d8b18..f66883c20 100644 --- a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs @@ -13,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Pixel1 = 1, + /// + /// 2 bits per pixel. + /// + Pixel2 = 2, + /// /// 4 bits per pixel. /// diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 3a96c4022..517a3b8cf 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -1387,7 +1387,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp int colorMapSizeBytes = -1; if (this.infoHeader.ClrUsed == 0) { - if (this.infoHeader.BitsPerPixel is 1 or 4 or 8) + if (this.infoHeader.BitsPerPixel is 1 or 2 or 4 or 8) { switch (this.fileMarkerType) { diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index f71275b7c..257159bd2 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -57,6 +57,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// private const int ColorPaletteSize4Bit = 64; + /// + /// The color palette for an 2 bit image will have 4 entry's with 4 bytes for each entry. + /// + private const int ColorPaletteSize2Bit = 16; + /// /// The color palette for an 1 bit image will have 2 entry's with 4 bytes for each entry. /// @@ -125,19 +130,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32); this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F)); - int colorPaletteSize = 0; - if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8) - { - colorPaletteSize = ColorPaletteSize8Bit; - } - else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4) - { - colorPaletteSize = ColorPaletteSize4Bit; - } - else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1) + int colorPaletteSize = this.bitsPerPixel switch { - colorPaletteSize = ColorPaletteSize1Bit; - } + BmpBitsPerPixel.Pixel8 => ColorPaletteSize8Bit, + BmpBitsPerPixel.Pixel4 => ColorPaletteSize4Bit, + BmpBitsPerPixel.Pixel2 => ColorPaletteSize2Bit, + BmpBitsPerPixel.Pixel1 => ColorPaletteSize1Bit, + _ => 0 + }; byte[] iccProfileData = null; int iccProfileSize = 0; @@ -322,27 +322,31 @@ namespace SixLabors.ImageSharp.Formats.Bmp switch (this.bitsPerPixel) { case BmpBitsPerPixel.Pixel32: - this.Write32Bit(stream, pixels); + this.Write32BitPixelData(stream, pixels); break; case BmpBitsPerPixel.Pixel24: - this.Write24Bit(stream, pixels); + this.Write24BitPixelData(stream, pixels); break; case BmpBitsPerPixel.Pixel16: - this.Write16Bit(stream, pixels); + this.Write16BitPixelData(stream, pixels); break; case BmpBitsPerPixel.Pixel8: - this.Write8Bit(stream, image); + this.Write8BitPixelData(stream, image); break; case BmpBitsPerPixel.Pixel4: - this.Write4BitColor(stream, image); + this.Write4BitPixelData(stream, image); + break; + + case BmpBitsPerPixel.Pixel2: + this.Write2BitPixelData(stream, image); break; case BmpBitsPerPixel.Pixel1: - this.Write1BitColor(stream, image); + this.Write1BitPixelData(stream, image); break; } } @@ -351,12 +355,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, this.padding); /// - /// Writes the 32bit color palette to the stream. + /// Writes 32-bit data with a color palette to the stream. /// /// The pixel format. /// The to write to. /// The containing pixel data. - private void Write32Bit(Stream stream, Buffer2D pixels) + private void Write32BitPixelData(Stream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { using IMemoryOwner row = this.AllocateRow(pixels.Width, 4); @@ -375,12 +379,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes the 24bit color palette to the stream. + /// Writes 24-bit pixel data with a color palette to the stream. /// /// The pixel format. /// The to write to. /// The containing pixel data. - private void Write24Bit(Stream stream, Buffer2D pixels) + private void Write24BitPixelData(Stream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -401,12 +405,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes the 16bit color palette to the stream. + /// Writes 16-bit pixel data with a color palette to the stream. /// /// The type of the pixel. /// The to write to. /// The containing pixel data. - private void Write16Bit(Stream stream, Buffer2D pixels) + private void Write16BitPixelData(Stream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -429,12 +433,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes an 8 bit image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// Writes 8 bit pixel data 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. - private void Write8Bit(Stream stream, ImageFrame image) + private void Write8BitPixelData(Stream stream, ImageFrame image) where TPixel : unmanaged, IPixel { bool isL8 = typeof(TPixel) == typeof(L8); @@ -443,7 +447,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp if (isL8) { - this.Write8BitGray(stream, image, colorPalette); + this.Write8BitPixelData(stream, image, colorPalette); } else { @@ -480,13 +484,13 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes an 8 bit gray image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. + /// Writes 8 bit gray pixel data 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) + private void Write8BitPixelData(Stream stream, ImageFrame image, Span colorPalette) where TPixel : unmanaged, IPixel { // Create a color palette with 256 different gray values. @@ -518,12 +522,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes an 4 bit color image with a color palette. The color palette has 16 entry's with 4 bytes for each entry. + /// Writes 4 bit pixel data with a color palette. The color palette has 16 entry's with 4 bytes for each entry. /// /// The type of the pixel. /// The to write to. /// The containing pixel data. - private void Write4BitColor(Stream stream, ImageFrame image) + private void Write4BitPixelData(Stream stream, ImageFrame image) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions() @@ -562,12 +566,65 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Writes a 1 bit image with a color palette. The color palette has 2 entry's with 4 bytes for each entry. + /// Writes 2 bit pixel data with a color palette. The color palette has 4 entry's with 4 bytes for each entry. + /// + /// The type of the pixel. + /// The to write to. + /// The containing pixel data. + private void Write2BitPixelData(Stream stream, ImageFrame image) + where TPixel : unmanaged, IPixel + { + using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions() + { + MaxColors = 4 + }); + using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds()); + using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.Allocate(ColorPaletteSize2Bit, AllocationOptions.Clean); + + Span colorPalette = colorPaletteBuffer.GetSpan(); + ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; + this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); + + ReadOnlySpan pixelRowSpan = quantized.DangerousGetRowSpan(0); + int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding; + for (int y = image.Height - 1; y >= 0; y--) + { + pixelRowSpan = quantized.DangerousGetRowSpan(y); + + int endIdx = pixelRowSpan.Length % 4 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 4; + int i = 0; + for (i = 0; i < endIdx; i += 4) + { + stream.WriteByte((byte)((pixelRowSpan[i] << 6) | (pixelRowSpan[i + 1] << 4) | (pixelRowSpan[i + 2] << 2) | pixelRowSpan[i + 3])); + } + + if (pixelRowSpan.Length % 4 != 0) + { + int shift = 6; + byte pixelData = 0; + for (; i < pixelRowSpan.Length; i++) + { + pixelData = (byte)(pixelData | (pixelRowSpan[i] << shift)); + shift -= 2; + } + + stream.WriteByte(pixelData); + } + + for (i = 0; i < rowPadding; i++) + { + stream.WriteByte(0); + } + } + } + + /// + /// Writes 1 bit pixel data with a color palette. The color palette has 2 entry's with 4 bytes for each entry. /// /// The type of the pixel. /// The to write to. /// The containing pixel data. - private void Write1BitColor(Stream stream, ImageFrame image) + private void Write1BitPixelData(Stream stream, ImageFrame image) where TPixel : unmanaged, IPixel { using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions() @@ -622,7 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp Span colorPaletteAsUInt = MemoryMarshal.Cast(colorPalette); for (int i = 0; i < colorPaletteAsUInt.Length; i++) { - colorPaletteAsUInt[i] = colorPaletteAsUInt[i] & 0x00FFFFFF; // Padding byte, always 0. + colorPaletteAsUInt[i] &= 0x00FFFFFF; // Padding byte, always 0. } stream.Write(colorPalette); diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index dd59fb279..fc9554f6a 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -20,6 +20,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp [Trait("Format", "Bmp")] public class BmpEncoderTests { + private static BmpDecoder BmpDecoder => new(); + public static readonly TheoryData BitsPerPixel = new() { @@ -39,6 +41,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp new() { { Bit1, BmpBitsPerPixel.Pixel1 }, + { Bit2, BmpBitsPerPixel.Pixel2 }, { Bit4, BmpBitsPerPixel.Pixel4 }, { Bit8, BmpBitsPerPixel.Pixel8 }, { Rgb16, BmpBitsPerPixel.Pixel16 }, @@ -204,6 +207,50 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp 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); + + // 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); + + // 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( @@ -343,7 +390,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp BmpBitsPerPixel bitsPerPixel, bool supportTransparency = true, // if set to true, will write a V4 header, otherwise a V3 header. IQuantizer quantizer = null, - ImageComparer customComparer = null) + ImageComparer customComparer = null, + IImageDecoder referenceDecoder = null) where TPixel : unmanaged, IPixel { using (Image image = provider.GetImage()) @@ -362,7 +410,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp }; // Does DebugSave & load reference CompareToReferenceInput(): - image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder, customComparer); + image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder, customComparer, referenceDecoder: referenceDecoder); } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 306a28dae..27df82b5f 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -344,6 +344,8 @@ namespace SixLabors.ImageSharp.Tests public const string RLE4Delta = "Bmp/pal4rletrns.bmp"; public const string Rle4Delta320240 = "Bmp/rle4-delta-320x240.bmp"; public const string Bit1 = "Bmp/pal1.bmp"; + public const string Bit2 = "Bmp/pal2.bmp"; + public const string Bit2Color = "Bmp/pal2Color.bmp"; public const string Bit1Pal1 = "Bmp/pal1p1.bmp"; public const string Bit4 = "Bmp/pal4.bmp"; public const string Bit8 = "Bmp/test8.bmp"; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs index cea2784b6..d2750c31c 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs @@ -19,10 +19,8 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison /// A ImageComparer instance. public static ImageComparer Tolerant( float imageThreshold = TolerantImageComparer.DefaultImageThreshold, - int perPixelManhattanThreshold = 0) - { - return new TolerantImageComparer(imageThreshold, perPixelManhattanThreshold); - } + int perPixelManhattanThreshold = 0) => + new TolerantImageComparer(imageThreshold, perPixelManhattanThreshold); /// /// Returns Tolerant(imageThresholdInPercents/100) @@ -45,10 +43,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison Image expected, Image actual) where TPixelA : unmanaged, IPixel - where TPixelB : unmanaged, IPixel - { - return comparer.CompareImagesOrFrames(expected.Frames.RootFrame, actual.Frames.RootFrame); - } + where TPixelB : unmanaged, IPixel => comparer.CompareImagesOrFrames(expected.Frames.RootFrame, actual.Frames.RootFrame); public static IEnumerable> CompareImages( this ImageComparer comparer, diff --git a/tests/Images/Input/Bmp/pal2.bmp b/tests/Images/Input/Bmp/pal2.bmp new file mode 100644 index 000000000..ac351d5fb --- /dev/null +++ b/tests/Images/Input/Bmp/pal2.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bac6eec4100831e635fcd34a9e0e34a8a9082abdec132ac327aa1bfc7137d40f +size 2118 diff --git a/tests/Images/Input/Bmp/pal2color.bmp b/tests/Images/Input/Bmp/pal2color.bmp new file mode 100644 index 000000000..dd7c31bf6 --- /dev/null +++ b/tests/Images/Input/Bmp/pal2color.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ac541592afb207524091aa19d59614851c293193600eacb1170b4854d351dae +size 2118