diff --git a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
index 38f5c1d66..6e1145beb 100644
--- a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
@@ -4,10 +4,15 @@
namespace SixLabors.ImageSharp.Formats.Bmp
{
///
- /// Enumerates the available bits per pixel for bitmap.
+ /// Enumerates the available bits per pixel the bitmap encoder supports.
///
public enum BmpBitsPerPixel : short
{
+ ///
+ /// 8 bits per pixel. Each pixel consists of 1 byte.
+ ///
+ Pixel8 = 8,
+
///
/// 16 bits per pixel. Each pixel consists of 2 bytes.
///
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 9d9c7b624..0cbc4fca1 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -1022,7 +1022,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.bmpMetadata.InfoHeaderType = infoHeaderType;
// We can only encode at these bit rates so far.
- if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel16)
+ if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel8)
+ || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel16)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32))
{
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
index 4efdedb34..612675c33 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
@@ -4,13 +4,13 @@
using System.IO;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp
{
///
/// Image encoder for writing an image to a stream as a Windows bitmap.
///
- /// The encoder can currently only write 24-bit rgb images to streams.
public sealed class BmpEncoder : IImageEncoder, IBmpEncoderOptions
{
///
@@ -26,6 +26,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
public bool SupportTransparency { get; set; }
+ ///
+ /// Gets or sets the quantizer for reducing the color count for 8-Bit images.
+ /// Defaults to OctreeQuantizer.
+ ///
+ public IQuantizer Quantizer { get; set; }
+
///
public void Encode(Image image, Stream stream)
where TPixel : struct, IPixel
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 82483e390..90ea673d3 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Buffers;
using System.IO;
using SixLabors.ImageSharp.Advanced;
@@ -9,6 +10,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Formats.Bmp
@@ -43,6 +45,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
private const int Rgba32BlueMask = 0xFF;
+ ///
+ /// The color palette for an 8 bit image will have 256 entry's with 4 bytes for each entry.
+ ///
+ private const int ColorPaletteSize8Bit = 1024;
+
private readonly MemoryAllocator memoryAllocator;
private Configuration configuration;
@@ -56,6 +63,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
private readonly bool writeV4Header;
+ ///
+ /// The quantizer for reducing the color count for 8-Bit images.
+ ///
+ private readonly IQuantizer quantizer;
+
///
/// Initializes a new instance of the class.
///
@@ -66,6 +78,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel;
this.writeV4Header = options.SupportTransparency;
+ this.quantizer = options.Quantizer ?? new OctreeQuantizer(dither: true, maxColors: 256);
}
///
@@ -142,11 +155,13 @@ namespace SixLabors.ImageSharp.Formats.Bmp
infoHeader.Compression = BmpCompression.BitFields;
}
+ int colorPaletteSize = this.bitsPerPixel == BmpBitsPerPixel.Pixel8 ? ColorPaletteSize8Bit : 0;
+
var fileHeader = new BmpFileHeader(
type: BmpConstants.TypeMarkers.Bitmap,
fileSize: BmpFileHeader.Size + infoHeaderSize + infoHeader.ImageSize,
reserved: 0,
- offset: BmpFileHeader.Size + infoHeaderSize);
+ offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize);
#if NETCOREAPP2_1
Span buffer = stackalloc byte[infoHeaderSize];
@@ -198,6 +213,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
case BmpBitsPerPixel.Pixel16:
this.Write16Bit(stream, pixels);
break;
+
+ case BmpBitsPerPixel.Pixel8:
+ this.Write8Bit(stream, image);
+ break;
}
}
@@ -276,5 +295,47 @@ 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.
+ ///
+ /// The type of the pixel.
+ /// The to write to.
+ /// The containing pixel data.
+ private void Write8Bit(Stream stream, ImageFrame image)
+ where TPixel : struct, IPixel
+ {
+ using (IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize8Bit, AllocationOptions.Clean))
+ using (QuantizedFrame quantized = this.quantizer.CreateFrameQuantizer(this.configuration, 256).QuantizeFrame(image))
+ {
+ Span colorPalette = colorPaletteBuffer.GetSpan();
+ int idx = 0;
+ var color = default(Rgba32);
+ foreach (TPixel quantizedColor in quantized.Palette)
+ {
+ quantizedColor.ToRgba32(ref color);
+ colorPalette[idx] = color.B;
+ colorPalette[idx + 1] = color.G;
+ colorPalette[idx + 2] = color.R;
+
+ // Padding byte, always 0
+ colorPalette[idx + 3] = 0;
+ idx += 4;
+ }
+
+ stream.Write(colorPalette);
+
+ for (int y = image.Height - 1; y >= 0; y--)
+ {
+ Span pixelSpan = quantized.GetRowSpan(y);
+ stream.Write(pixelSpan);
+
+ for (int i = 0; i < this.padding; i++)
+ {
+ stream.WriteByte(0);
+ }
+ }
+ }
+ }
}
}
diff --git a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs
index 96ec423e7..59ad929df 100644
--- a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs
+++ b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs
@@ -1,12 +1,13 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
+
namespace SixLabors.ImageSharp.Formats.Bmp
{
///
- /// Configuration options for use during bmp encoding
+ /// Configuration options for use during bmp encoding.
///
- /// The encoder can currently only write 16-bit, 24-bit and 32-bit rgb images to streams.
internal interface IBmpEncoderOptions
{
///
@@ -21,5 +22,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// Instead a bitmap version 4 info header will be written with the BITFIELDS compression.
///
bool SupportTransparency { get; }
+
+ ///
+ /// Gets the quantizer for reducing the color count for 8-Bit images.
+ ///
+ IQuantizer Quantizer { get; }
}
}
\ No newline at end of file
diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
index d49023886..f5fa8c95d 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
@@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
///
/// Initializes a new instance of the class.
///
- /// Whether to apply dithering to the output image
+ /// Whether to apply dithering to the output image.
public OctreeQuantizer(bool dither)
: this(GetDiffuser(dither), QuantizerConstants.MaxColors)
{
@@ -44,7 +44,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
///
/// Initializes a new instance of the class.
///
- /// The error diffusion algorithm, if any, to apply to the output image
+ /// The maximum number of colors to hold in the color palette.
+ /// Whether to apply dithering to the output image.
+ public OctreeQuantizer(bool dither, int maxColors)
+ : this(GetDiffuser(dither), maxColors)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error diffusion algorithm, if any, to apply to the output image.
public OctreeQuantizer(IErrorDiffuser diffuser)
: this(diffuser, QuantizerConstants.MaxColors)
{
@@ -53,8 +63,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
///
/// Initializes a new instance of the class.
///
- /// The error diffusion algorithm, if any, to apply to the output image
- /// The maximum number of colors to hold in the color palette
+ /// The error diffusion algorithm, if any, to apply to the output image.
+ /// The maximum number of colors to hold in the color palette.
public OctreeQuantizer(IErrorDiffuser diffuser, int maxColors)
{
this.Diffuser = diffuser;
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
index df3029a7f..abf57ed01 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
@@ -2,10 +2,12 @@
// Licensed under the Apache License, Version 2.0.
using System.IO;
+
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
using Xunit.Abstractions;
@@ -110,11 +112,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
[WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)]
[WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)]
[WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)]
- // WinBmpv3 is a 24 bits per pixel image
- [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)]
- [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)]
- [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)]
- public void Encode_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ public void Encode_32Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
// if supportTransparency is false, a v3 bitmap header will be written
where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false);
@@ -123,24 +121,84 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
[WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)]
[WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)]
[WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)]
+ public void Encode_32Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
+
+ [Theory]
+ // WinBmpv3 is a 24 bits per pixel image
+ [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)]
+ [WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)]
+ public void Encode_24Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false);
+
+ [Theory]
[WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)]
+ [WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)]
+ public void Encode_24Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
+
+
+ [Theory]
+ [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)]
+ [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)]
+ public void Encode_16Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false);
+
+ [Theory]
[WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)]
[WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)]
- public void Encode_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ public void Encode_16Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
+
+ [Theory]
+ [WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)]
+ [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)]
+ public void Encode_8Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false);
+
+ [Theory]
+ [WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)]
+ [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)]
+ public void Encode_8Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
+ [Theory]
+ [WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)]
+ public void Encode_8BitGray_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ where TPixel : struct, IPixel =>
+ TestBmpEncoderCore(
+ provider,
+ bitsPerPixel,
+ supportTransparency: false,
+ ImageComparer.TolerantPercentage(0.01f));
+
+ [Theory]
+ [WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)]
+ public void Encode_8BitGray_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
+ where TPixel : struct, IPixel =>
+ TestBmpEncoderCore(
+ provider,
+ bitsPerPixel,
+ supportTransparency: true,
+ ImageComparer.TolerantPercentage(0.01f));
+
[Theory]
[WithFile(TestImages.Png.GrayAlpha2BitInterlaced, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)]
+ [WithFile(Bit32Rgba, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)]
public void Encode_PreservesAlpha(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
- private static void TestBmpEncoderCore(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel, bool supportTransparency = true)
+ private static void TestBmpEncoderCore(
+ TestImageProvider provider,
+ BmpBitsPerPixel bitsPerPixel,
+ bool supportTransparency = true,
+ ImageComparer customComparer = null)
where TPixel : struct, IPixel
{
using (Image image = provider.GetImage())
{
- // There is no alpha in bmp with 24 bits per pixels, so the reference image will be made opaque.
- if (bitsPerPixel == BmpBitsPerPixel.Pixel24)
+ // There is no alpha in bmp with less then 32 bits per pixels, so the reference image will be made opaque.
+ if (bitsPerPixel != BmpBitsPerPixel.Pixel32)
{
image.Mutate(c => c.MakeOpaque());
}
@@ -148,7 +206,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
var encoder = new BmpEncoder { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency };
// Does DebugSave & load reference CompareToReferenceInput():
- image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder);
+ image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder, customComparer);
}
}
}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index f82278ef5..62b7ae2ec 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -236,6 +236,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Bit1Pal1 = "Bmp/pal1p1.bmp";
public const string Bit4 = "Bmp/pal4.bmp";
public const string Bit8 = "Bmp/test8.bmp";
+ public const string Bit8Gs = "Bmp/pal8gs.bmp";
public const string Bit8Inverted = "Bmp/test8-inverted.bmp";
public const string Bit16 = "Bmp/test16.bmp";
public const string Bit16Inverted = "Bmp/test16-inverted.bmp";
diff --git a/tests/ImageSharp.Tests/TestUtilities/PixelTypes.cs b/tests/ImageSharp.Tests/TestUtilities/PixelTypes.cs
index e4a7572d6..78431f31a 100644
--- a/tests/ImageSharp.Tests/TestUtilities/PixelTypes.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/PixelTypes.cs
@@ -60,6 +60,8 @@ namespace SixLabors.ImageSharp.Tests
Bgra5551 = 1 << 22,
+ Gray8 = 1 << 23,
+
// TODO: Add multi-flag entries by rules defined in PackedPixelConverterHelper
// "All" is handled as a separate, individual case instead of using bitwise OR
diff --git a/tests/Images/Input/Bmp/pal8gs.bmp b/tests/Images/Input/Bmp/pal8gs.bmp
new file mode 100644
index 000000000..359499c7a
--- /dev/null
+++ b/tests/Images/Input/Bmp/pal8gs.bmp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:abb09008dc6af0b33db70ed01e4183f946cc90b647bd84b078794b2d97eb9c33
+size 9254