From 3431ad321ed48e440712b60fe1ca227708e3ee58 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 7 May 2019 19:58:14 +0200 Subject: [PATCH] Add support for encoding 8-bit bitmaps --- src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs | 7 +- src/ImageSharp/Formats/Bmp/BmpEncoder.cs | 1 - src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 60 +++++++++++++++++- .../Formats/Bmp/IBmpEncoderOptions.cs | 1 - .../Quantization/QuantizedFrame{TPixel}.cs | 2 +- .../Formats/Bmp/BmpEncoderTests.cs | 9 ++- tests/ImageSharp.Tests/TestImages.cs | 1 + .../TestUtilities/PixelTypes.cs | 2 + tests/Images/Input/Bmp/pal8gs.bmp | Bin 0 -> 9254 bytes 9 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 tests/Images/Input/Bmp/pal8gs.bmp 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/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index 4efdedb34..f3eb9282c 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -10,7 +10,6 @@ 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 { /// diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 82483e390..530bcf8cd 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -9,6 +9,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 +44,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; @@ -142,11 +148,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 +206,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp case BmpBitsPerPixel.Pixel16: this.Write16Bit(stream, pixels); break; + + case BmpBitsPerPixel.Pixel8: + this.Write8Bit(stream, image); + break; } } @@ -276,5 +288,51 @@ 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 + { +#if NETCOREAPP2_1 + Span colorPalette = stackalloc byte[ColorPaletteSize8Bit]; +#else + byte[] colorPalette = new byte[ColorPaletteSize8Bit]; +#endif + + var quantizer = new WuQuantizer(256); + QuantizedFrame quantized = quantizer.CreateFrameQuantizer(this.configuration).QuantizeFrame(image); + + 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, 0, ColorPaletteSize8Bit); + + 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..92ff09270 100644 --- a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs +++ b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs @@ -6,7 +6,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// 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 { /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs index 38862ef44..30b8bc846 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs @@ -62,7 +62,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Gets the representation of the pixels as a of contiguous memory - /// at row beginning from the the first pixel on that row. + /// at row beginning from the first pixel on that row. /// /// The row. /// The diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 7e054734e..42ec3088d 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -114,6 +114,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] + [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] + [WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)] public void Encode_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); @@ -126,11 +128,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp [WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] [WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] [WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] + [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] + [WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)] public void Encode_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [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); @@ -139,8 +144,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp { 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()); } 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 0000000000000000000000000000000000000000..66a0d70dc34f6c25554ea15d779f6114bd042f74 GIT binary patch literal 9254 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