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