Browse Source

Add option to write 2 bit bitmap's

pull/2220/head
Brian Popow 3 years ago
parent
commit
7356edd4ea
  1. 5
      src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
  2. 2
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  3. 125
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  4. 52
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  5. 2
      tests/ImageSharp.Tests/TestImages.cs
  6. 11
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs
  7. 3
      tests/Images/Input/Bmp/pal2.bmp
  8. 3
      tests/Images/Input/Bmp/pal2color.bmp

5
src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs

@ -13,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
Pixel1 = 1,
/// <summary>
/// 2 bits per pixel.
/// </summary>
Pixel2 = 2,
/// <summary>
/// 4 bits per pixel.
/// </summary>

2
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)
{

125
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -57,6 +57,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
private const int ColorPaletteSize4Bit = 64;
/// <summary>
/// The color palette for an 2 bit image will have 4 entry's with 4 bytes for each entry.
/// </summary>
private const int ColorPaletteSize2Bit = 16;
/// <summary>
/// The color palette for an 1 bit image will have 2 entry's with 4 bytes for each entry.
/// </summary>
@ -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);
/// <summary>
/// Writes the 32bit color palette to the stream.
/// Writes 32-bit data with a color palette to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write32Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
private void Write32BitPixelData<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4);
@ -375,12 +379,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
/// <summary>
/// Writes the 24bit color palette to the stream.
/// Writes 24-bit pixel data with a color palette to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write24Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
private void Write24BitPixelData<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;
@ -401,12 +405,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
/// <summary>
/// Writes the 16bit color palette to the stream.
/// Writes 16-bit pixel data with a color palette to the stream.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write16Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
private void Write16BitPixelData<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;
@ -429,12 +433,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write8Bit<TPixel>(Stream stream, ImageFrame<TPixel> image)
private void Write8BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
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
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitGray<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
private void Write8BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
// Create a color palette with 256 different gray values.
@ -518,12 +522,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write4BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image)
private void Write4BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
@ -562,12 +566,65 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write2BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
{
MaxColors = 4
});
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.Allocate<byte>(ColorPaletteSize2Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);
ReadOnlySpan<byte> 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);
}
}
}
/// <summary>
/// Writes 1 bit pixel data with a color palette. The color palette has 2 entry's with 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write1BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image)
private void Write1BitPixelData<TPixel>(Stream stream, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
@ -622,7 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
Span<uint> colorPaletteAsUInt = MemoryMarshal.Cast<byte, uint>(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);

52
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<BmpBitsPerPixel> 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<TPixel>(
TestImageProvider<TPixel> provider,
BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel };
using var memoryStream = new MemoryStream();
using Image<TPixel> input = provider.GetImage(BmpDecoder);
// act
encoder.Encode(input, memoryStream);
memoryStream.Position = 0;
// assert
using var actual = Image.Load<TPixel>(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<TPixel>(
TestImageProvider<TPixel> provider,
BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel };
using var memoryStream = new MemoryStream();
using Image<TPixel> input = provider.GetImage(BmpDecoder);
// act
encoder.Encode(input, memoryStream);
memoryStream.Position = 0;
// assert
using var actual = Image.Load<TPixel>(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<TPixel>(
@ -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<TPixel>
{
using (Image<TPixel> 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);
}
}
}

2
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";

11
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs

@ -19,10 +19,8 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
/// <returns>A ImageComparer instance.</returns>
public static ImageComparer Tolerant(
float imageThreshold = TolerantImageComparer.DefaultImageThreshold,
int perPixelManhattanThreshold = 0)
{
return new TolerantImageComparer(imageThreshold, perPixelManhattanThreshold);
}
int perPixelManhattanThreshold = 0) =>
new TolerantImageComparer(imageThreshold, perPixelManhattanThreshold);
/// <summary>
/// Returns Tolerant(imageThresholdInPercents/100)
@ -45,10 +43,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
Image<TPixelA> expected,
Image<TPixelB> actual)
where TPixelA : unmanaged, IPixel<TPixelA>
where TPixelB : unmanaged, IPixel<TPixelB>
{
return comparer.CompareImagesOrFrames(expected.Frames.RootFrame, actual.Frames.RootFrame);
}
where TPixelB : unmanaged, IPixel<TPixelB> => comparer.CompareImagesOrFrames(expected.Frames.RootFrame, actual.Frames.RootFrame);
public static IEnumerable<ImageSimilarityReport<TPixelA, TPixelB>> CompareImages<TPixelA, TPixelB>(
this ImageComparer comparer,

3
tests/Images/Input/Bmp/pal2.bmp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bac6eec4100831e635fcd34a9e0e34a8a9082abdec132ac327aa1bfc7137d40f
size 2118

3
tests/Images/Input/Bmp/pal2color.bmp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ac541592afb207524091aa19d59614851c293193600eacb1170b4854d351dae
size 2118
Loading…
Cancel
Save