diff --git a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs index 6fdf8d634..87f3b7e31 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,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public enum BmpBitsPerPixel : short { + /// + /// 4 bits per pixel. + /// + Pixel4 = 4, + /// /// 8 bits per pixel. Each pixel consists of 1 byte. /// @@ -28,4 +33,4 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Pixel32 = 32 } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 7819b1ebd..7733b718a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -51,6 +51,11 @@ 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; + /// /// Used for allocating memory during processing operations. /// @@ -107,7 +112,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 +171,15 @@ 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; + } var fileHeader = new BmpFileHeader( type: BmpConstants.TypeMarkers.Bitmap, @@ -224,6 +237,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp case BmpBitsPerPixel.Pixel8: this.Write8Bit(stream, image); break; + + case BmpBitsPerPixel.Pixel4: + this.Write4BitColor(stream, image); + break; } } @@ -344,16 +361,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--) { @@ -404,5 +413,70 @@ 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 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); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index fa63642bd..35853413e 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,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public static readonly TheoryData BmpBitsPerPixelFiles = new TheoryData { + { 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 +171,16 @@ 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 => 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 => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); + [Theory] [WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)] public void Encode_8BitGray_WithV4Header_Works(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)