diff --git a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs index 87f3b7e319..7801e48a91 100644 --- a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs @@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public enum BmpBitsPerPixel : short { + /// + /// 1 bit per pixel. + /// + Pixel1 = 1, + /// /// 4 bits per pixel. /// diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 17ba96312a..f6fefda485 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -1303,16 +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 per pixel is still missing). - if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel4) - || 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 7733b718ae..5cf54388d3 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -56,6 +56,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// 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. /// @@ -79,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; @@ -93,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; } /// @@ -180,6 +185,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp { colorPaletteSize = ColorPaletteSize4Bit; } + else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1) + { + colorPaletteSize = ColorPaletteSize1Bit; + } var fileHeader = new BmpFileHeader( type: BmpConstants.TypeMarkers.Bitmap, @@ -241,6 +250,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp case BmpBitsPerPixel.Pixel4: this.Write4BitColor(stream, image); break; + + case BmpBitsPerPixel.Pixel1: + this.Write1BitColor(stream, image); + break; } } @@ -325,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. @@ -349,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. @@ -377,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. @@ -415,7 +428,7 @@ 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 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. @@ -458,6 +471,52 @@ 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. + /// + /// 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. /// @@ -478,5 +537,25 @@ namespace SixLabors.ImageSharp.Formats.Bmp 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 f439b3b199..4eb3b900e1 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -40,6 +40,7 @@ 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 }, @@ -201,6 +202,34 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp } } + [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) @@ -297,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 { @@ -309,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);