diff --git a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
index 6fdf8d6342..7801e48a91 100644
--- a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Bmp
@@ -8,6 +8,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
public enum BmpBitsPerPixel : short
{
+ ///
+ /// 1 bit per pixel.
+ ///
+ Pixel1 = 1,
+
+ ///
+ /// 4 bits per pixel.
+ ///
+ Pixel4 = 4,
+
///
/// 8 bits per pixel. Each pixel consists of 1 byte.
///
@@ -28,4 +38,4 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
Pixel32 = 32
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 0be0385725..f6fefda485 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -1303,15 +1303,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
short bitsPerPixel = this.infoHeader.BitsPerPixel;
this.bmpMetadata = this.metadata.GetBmpMetadata();
this.bmpMetadata.InfoHeaderType = infoHeaderType;
-
- // We can only encode at these bit rates so far (1 bit and 4 bit are still missing).
- if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel8)
- || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel16)
- || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24)
- || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32))
- {
- this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
- }
+ this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
}
///
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
index 2f5c4b7cf7..f256ed9f81 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
@@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Gets or sets the quantizer for reducing the color count for 8-Bit images.
- /// Defaults to OctreeQuantizer.
+ /// Defaults to Wu Quantizer.
///
public IQuantizer Quantizer { get; set; }
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 7819b1ebdb..5cf54388d3 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -51,6 +51,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
private const int ColorPaletteSize8Bit = 1024;
+ ///
+ /// The color palette for an 4 bit image will have 16 entry's with 4 bytes for each entry.
+ ///
+ private const int ColorPaletteSize4Bit = 64;
+
+ ///
+ /// The color palette for an 1 bit image will have 2 entry's with 4 bytes for each entry.
+ ///
+ private const int ColorPaletteSize1Bit = 8;
+
///
/// Used for allocating memory during processing operations.
///
@@ -74,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private readonly bool writeV4Header;
///
- /// The quantizer for reducing the color count for 8-Bit images.
+ /// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images.
///
private readonly IQuantizer quantizer;
@@ -88,7 +98,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel;
this.writeV4Header = options.SupportTransparency;
- this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
+ this.quantizer = options.Quantizer ?? KnownQuantizers.Wu;
}
///
@@ -107,7 +117,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.configuration = image.GetConfiguration();
ImageMetadata metadata = image.Metadata;
BmpMetadata bmpMetadata = metadata.GetBmpMetadata();
- this.bitsPerPixel = this.bitsPerPixel ?? bmpMetadata.BitsPerPixel;
+ this.bitsPerPixel ??= bmpMetadata.BitsPerPixel;
short bpp = (short)this.bitsPerPixel;
int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32);
@@ -166,7 +176,19 @@ namespace SixLabors.ImageSharp.Formats.Bmp
infoHeader.Compression = BmpCompression.BitFields;
}
- int colorPaletteSize = this.bitsPerPixel == BmpBitsPerPixel.Pixel8 ? ColorPaletteSize8Bit : 0;
+ int colorPaletteSize = 0;
+ if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8)
+ {
+ colorPaletteSize = ColorPaletteSize8Bit;
+ }
+ else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4)
+ {
+ colorPaletteSize = ColorPaletteSize4Bit;
+ }
+ else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1)
+ {
+ colorPaletteSize = ColorPaletteSize1Bit;
+ }
var fileHeader = new BmpFileHeader(
type: BmpConstants.TypeMarkers.Bitmap,
@@ -224,6 +246,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp
case BmpBitsPerPixel.Pixel8:
this.Write8Bit(stream, image);
break;
+
+ case BmpBitsPerPixel.Pixel4:
+ this.Write4BitColor(stream, image);
+ break;
+
+ case BmpBitsPerPixel.Pixel1:
+ this.Write1BitColor(stream, image);
+ break;
}
}
@@ -308,7 +338,7 @@ 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 an 8 bit 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.
@@ -332,7 +362,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
///
- /// Writes an 8 Bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
+ /// 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.
@@ -344,16 +374,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration);
using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
- ReadOnlySpan quantizedColors = quantized.Palette.Span;
- var quantizedColorBytes = quantizedColors.Length * 4;
- PixelOperations.Instance.ToBgra32(this.configuration, quantizedColors, MemoryMarshal.Cast(colorPalette.Slice(0, quantizedColorBytes)));
- Span colorPaletteAsUInt = MemoryMarshal.Cast(colorPalette);
- for (int i = 0; i < colorPaletteAsUInt.Length; i++)
- {
- colorPaletteAsUInt[i] = colorPaletteAsUInt[i] & 0x00FFFFFF; // Padding byte, always 0.
- }
-
- stream.Write(colorPalette);
+ ReadOnlySpan quantizedColorPalette = quantized.Palette.Span;
+ this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);
for (int y = image.Height - 1; y >= 0; y--)
{
@@ -368,7 +390,7 @@ 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 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.
@@ -404,5 +426,136 @@ 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.
+ ///
+ /// The type of the pixel.
+ /// The to write to.
+ /// The containing pixel data.
+ private void Write4BitColor(Stream stream, ImageFrame image)
+ where TPixel : unmanaged, IPixel
+ {
+ using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions()
+ {
+ MaxColors = 16
+ });
+ using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
+ using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize4Bit, AllocationOptions.Clean);
+
+ Span colorPalette = colorPaletteBuffer.GetSpan();
+ ReadOnlySpan quantizedColorPalette = quantized.Palette.Span;
+ this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);
+
+ ReadOnlySpan pixelRowSpan = quantized.GetPixelRowSpan(0);
+ int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding;
+ for (int y = image.Height - 1; y >= 0; y--)
+ {
+ pixelRowSpan = quantized.GetPixelRowSpan(y);
+
+ int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1;
+ for (int i = 0; i < endIdx; i += 2)
+ {
+ stream.WriteByte((byte)((pixelRowSpan[i] << 4) | pixelRowSpan[i + 1]));
+ }
+
+ if (pixelRowSpan.Length % 2 != 0)
+ {
+ stream.WriteByte((byte)((pixelRowSpan[pixelRowSpan.Length - 1] << 4) | 0));
+ }
+
+ for (int i = 0; i < rowPadding; i++)
+ {
+ stream.WriteByte(0);
+ }
+ }
+ }
+
+ ///
+ /// Writes a 1 bit image 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)
+ where TPixel : unmanaged, IPixel
+ {
+ using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, new QuantizerOptions()
+ {
+ MaxColors = 2
+ });
+ using IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
+ using IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize1Bit, AllocationOptions.Clean);
+
+ Span colorPalette = colorPaletteBuffer.GetSpan();
+ ReadOnlySpan quantizedColorPalette = quantized.Palette.Span;
+ this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);
+
+ ReadOnlySpan quantizedPixelRow = quantized.GetPixelRowSpan(0);
+ int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding;
+ for (int y = image.Height - 1; y >= 0; y--)
+ {
+ quantizedPixelRow = quantized.GetPixelRowSpan(y);
+
+ int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8;
+ for (int i = 0; i < endIdx; i += 8)
+ {
+ Write1BitPalette(stream, i, i + 8, quantizedPixelRow);
+ }
+
+ if (quantizedPixelRow.Length % 8 != 0)
+ {
+ int startIdx = quantizedPixelRow.Length - 7;
+ endIdx = quantizedPixelRow.Length;
+ Write1BitPalette(stream, startIdx, endIdx, quantizedPixelRow);
+ }
+
+ for (int i = 0; i < rowPadding; i++)
+ {
+ stream.WriteByte(0);
+ }
+ }
+ }
+
+ ///
+ /// Writes the color palette to the stream. The color palette has 4 bytes for each entry.
+ ///
+ /// The type of the pixel.
+ /// The to write to.
+ /// The color palette from the quantized image.
+ /// A temporary byte span to write the color palette to.
+ private void WriteColorPalette(Stream stream, ReadOnlySpan quantizedColorPalette, Span colorPalette)
+ where TPixel : unmanaged, IPixel
+ {
+ int quantizedColorBytes = quantizedColorPalette.Length * 4;
+ PixelOperations.Instance.ToBgra32(this.configuration, quantizedColorPalette, MemoryMarshal.Cast(colorPalette.Slice(0, quantizedColorBytes)));
+ Span colorPaletteAsUInt = MemoryMarshal.Cast(colorPalette);
+ for (int i = 0; i < colorPaletteAsUInt.Length; i++)
+ {
+ colorPaletteAsUInt[i] = colorPaletteAsUInt[i] & 0x00FFFFFF; // Padding byte, always 0.
+ }
+
+ stream.Write(colorPalette);
+ }
+
+ ///
+ /// Writes a 1-bit palette.
+ ///
+ /// The stream to write the palette to.
+ /// The start index.
+ /// The end index.
+ /// A quantized pixel row.
+ private static void Write1BitPalette(Stream stream, int startIdx, int endIdx, ReadOnlySpan quantizedPixelRow)
+ {
+ int shift = 7;
+ byte indices = 0;
+ for (int j = startIdx; j < endIdx; j++)
+ {
+ indices = (byte)(indices | ((byte)(quantizedPixelRow[j] & 1) << shift));
+ shift--;
+ }
+
+ stream.WriteByte(indices);
+ }
}
}
diff --git a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs
index d4a22d66ea..30aa70452e 100644
--- a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs
+++ b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
bool SupportTransparency { get; }
///
- /// Gets the quantizer for reducing the color count for 8-Bit images.
+ /// Gets the quantizer for reducing the color count for 8-Bit, 4-Bit, and 1-Bit images.
///
IQuantizer Quantizer { get; }
}
-}
\ 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 fa63642bd2..4eb3b900e1 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
@@ -13,7 +13,6 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
using Xunit;
-using Xunit.Abstractions;
using static SixLabors.ImageSharp.Tests.TestImages.Bmp;
@@ -41,14 +40,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
public static readonly TheoryData BmpBitsPerPixelFiles =
new TheoryData
{
+ { Bit1, BmpBitsPerPixel.Pixel1 },
+ { Bit4, BmpBitsPerPixel.Pixel4 },
+ { Bit8, BmpBitsPerPixel.Pixel8 },
+ { Rgb16, BmpBitsPerPixel.Pixel16 },
{ Car, BmpBitsPerPixel.Pixel24 },
{ Bit32Rgb, BmpBitsPerPixel.Pixel32 }
};
- public BmpEncoderTests(ITestOutputHelper output) => this.Output = output;
-
- private ITestOutputHelper Output { get; }
-
[Theory]
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
@@ -175,6 +174,62 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
bitsPerPixel,
supportTransparency: false);
+ [Theory]
+ [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)]
+ public void Encode_4Bit_WithV3Header_Works(
+ TestImageProvider provider,
+ BmpBitsPerPixel bitsPerPixel)
+ where TPixel : unmanaged, IPixel
+ {
+ // The Magick Reference Decoder can not decode 4-Bit bitmaps, so only execute this on windows.
+ if (TestEnvironment.IsWindows)
+ {
+ TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false);
+ }
+ }
+
+ [Theory]
+ [WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)]
+ public void Encode_4Bit_WithV4Header_Works(
+ TestImageProvider provider,
+ BmpBitsPerPixel bitsPerPixel)
+ where TPixel : unmanaged, IPixel
+ {
+ // The Magick Reference Decoder can not decode 4-Bit bitmaps, so only execute this on windows.
+ if (TestEnvironment.IsWindows)
+ {
+ TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
+ }
+ }
+
+ [Theory]
+ [WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)]
+ public void Encode_1Bit_WithV3Header_Works(
+ TestImageProvider provider,
+ BmpBitsPerPixel bitsPerPixel)
+ where TPixel : unmanaged, IPixel
+ {
+ // The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows.
+ if (TestEnvironment.IsWindows)
+ {
+ 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
+ {
+ // The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows.
+ if (TestEnvironment.IsWindows)
+ {
+ TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
+ }
+ }
+
[Theory]
[WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)]
public void Encode_8BitGray_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
@@ -271,7 +326,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
private static void TestBmpEncoderCore(
TestImageProvider provider,
BmpBitsPerPixel bitsPerPixel,
- bool supportTransparency = true,
+ bool supportTransparency = true, // if set to true, will write a V4 header, otherwise a V3 header.
+ IQuantizer quantizer = null,
ImageComparer customComparer = null)
where TPixel : unmanaged, IPixel
{
@@ -283,7 +339,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
image.Mutate(c => c.MakeOpaque());
}
- var encoder = new BmpEncoder { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency };
+ var encoder = new BmpEncoder
+ {
+ BitsPerPixel = bitsPerPixel,
+ SupportTransparency = supportTransparency,
+ Quantizer = quantizer ?? KnownQuantizers.Wu
+ };
// Does DebugSave & load reference CompareToReferenceInput():
image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder, customComparer);