From fddae8734a48c5c157ce5682cea22fe2a251b724 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 7 May 2019 19:58:14 +0200 Subject: [PATCH 1/8] 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 | 3 + 9 files changed, 79 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 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 From fc9c716ece921fb76c1eebc13ab875aa1ce441cf Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 7 May 2019 20:43:12 +0200 Subject: [PATCH 2/8] Changed WuQuantizer to OctreeQuantizer --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 530bcf8cd..0ef2046a8 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -304,7 +304,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp byte[] colorPalette = new byte[ColorPaletteSize8Bit]; #endif - var quantizer = new WuQuantizer(256); + var quantizer = new OctreeQuantizer(256); QuantizedFrame quantized = quantizer.CreateFrameQuantizer(this.configuration).QuantizeFrame(image); int idx = 0; From 75d5bd812e917398ae90e16917ac6fcf154e0ee0 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 9 May 2019 20:35:19 +0200 Subject: [PATCH 3/8] Trying MagickReferenceDecoder for the 8bit greyscale image encoder tests --- .../Formats/Bmp/BmpEncoderTests.cs | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 42ec3088d..a56cee47d 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -2,10 +2,13 @@ // Licensed under the Apache License, Version 2.0. using System.IO; + +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; using Xunit.Abstractions; @@ -110,13 +113,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)] - [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] - [WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)] - 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); @@ -125,13 +122,44 @@ 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)] + 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)] + 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_16Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) + where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); + + [Theory] [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); + public void Encode_8Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) + where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false, referenceDecoder: new MagickReferenceDecoder()); + + [Theory] + [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] + [WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)] + public void Encode_8Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) + where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true, referenceDecoder: new MagickReferenceDecoder()); [Theory] [WithFile(TestImages.Png.GrayAlpha2BitInterlaced, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] @@ -139,7 +167,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp 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, IImageDecoder referenceDecoder = null) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -153,7 +181,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, referenceDecoder: referenceDecoder); } } } From 1140910d84c4bd23df5fb91507717d35139b69ed Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 9 May 2019 22:07:04 +0200 Subject: [PATCH 4/8] Setting dither to false in the OctreeQuantizer --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 2 +- .../Processors/Quantization/OctreeQuantizer.cs | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 0ef2046a8..be82e2cab 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -304,7 +304,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp byte[] colorPalette = new byte[ColorPaletteSize8Bit]; #endif - var quantizer = new OctreeQuantizer(256); + var quantizer = new OctreeQuantizer(dither: false, maxColors: 256); QuantizedFrame quantized = quantizer.CreateFrameQuantizer(this.configuration).QuantizeFrame(image); int idx = 0; 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; From 6a9123b09c016ed2d33648963ba880ccada550f1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 10 May 2019 20:28:46 +0200 Subject: [PATCH 5/8] Switched back to the default reference decoder --- tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index a56cee47d..f3d6e0fa8 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -153,13 +153,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] [WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)] public void Encode_8Bit_WithV3Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) - where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false, referenceDecoder: new MagickReferenceDecoder()); + where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); [Theory] [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] [WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)] public void Encode_8Bit_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) - where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true, referenceDecoder: new MagickReferenceDecoder()); + where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] [WithFile(TestImages.Png.GrayAlpha2BitInterlaced, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] @@ -167,7 +167,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp 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, IImageDecoder referenceDecoder = null) + private static void TestBmpEncoderCore(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel, bool supportTransparency = true) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -181,7 +181,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, referenceDecoder: referenceDecoder); + image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder); } } } From 8618a05f44f1a54a72b3ac2b81f9a5cd94066201 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 11 May 2019 13:52:18 +0200 Subject: [PATCH 6/8] Using tolerant comparer for the Bit8Gs image --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 3 +- .../Formats/Bmp/BmpEncoderTests.cs | 37 ++++++++++++++++--- 2 files changed, 33 insertions(+), 7 deletions(-) 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/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index f3d6e0fa8..1d489e69d 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -3,12 +3,11 @@ using System.IO; -using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; using Xunit.Abstractions; @@ -128,11 +127,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp [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); @@ -150,24 +151,48 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); [Theory] + [WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] [WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] - [WithFile(Bit8Gs, PixelTypes.Gray8, 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)] - [WithFile(Bit8Gs, PixelTypes.Gray8, 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()) @@ -181,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); } } } From 09d6fe7ccf59c6e2de8f15e0e8f15372f61b0552 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 11 May 2019 16:27:51 +0200 Subject: [PATCH 7/8] Enabled dither again --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index be82e2cab..8d0e8578d 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -304,7 +304,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp byte[] colorPalette = new byte[ColorPaletteSize8Bit]; #endif - var quantizer = new OctreeQuantizer(dither: false, maxColors: 256); + var quantizer = new OctreeQuantizer(dither: true, maxColors: 256); QuantizedFrame quantized = quantizer.CreateFrameQuantizer(this.configuration).QuantizeFrame(image); int idx = 0; From 7b81ea5e6bab2084d2f26da86dfa4052b3cab9dd Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 12 May 2019 18:38:22 +0200 Subject: [PATCH 8/8] Review changes --- src/ImageSharp/Formats/Bmp/BmpEncoder.cs | 7 +++ src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 61 ++++++++++--------- .../Formats/Bmp/IBmpEncoderOptions.cs | 9 ++- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index f3eb9282c..612675c33 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -4,6 +4,7 @@ using System.IO; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Bmp { @@ -25,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 8d0e8578d..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; @@ -62,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. /// @@ -72,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); } /// @@ -298,39 +305,35 @@ namespace SixLabors.ImageSharp.Formats.Bmp 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 OctreeQuantizer(dither: true, maxColors: 256); - QuantizedFrame quantized = quantizer.CreateFrameQuantizer(this.configuration).QuantizeFrame(image); - - int idx = 0; - var color = default(Rgba32); - foreach (TPixel quantizedColor in quantized.Palette) + using (IMemoryOwner colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize8Bit, AllocationOptions.Clean)) + using (QuantizedFrame quantized = this.quantizer.CreateFrameQuantizer(this.configuration, 256).QuantizeFrame(image)) { - 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); + 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; + } - for (int y = image.Height - 1; y >= 0; y--) - { - Span pixelSpan = quantized.GetRowSpan(y); - stream.Write(pixelSpan); + stream.Write(colorPalette); - for (int i = 0; i < this.padding; i++) + for (int y = image.Height - 1; y >= 0; y--) { - stream.WriteByte(0); + 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 92ff09270..59ad929df 100644 --- a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs +++ b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs @@ -1,10 +1,12 @@ // 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. /// internal interface IBmpEncoderOptions { @@ -20,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