Browse Source

Add support for encoding 4 bit per pixel bitmaps

pull/1622/head
Brian Popow 5 years ago
parent
commit
c05d4ddeb5
  1. 9
      src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
  2. 98
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  3. 16
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

9
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
/// </summary>
public enum BmpBitsPerPixel : short
{
/// <summary>
/// 4 bits per pixel.
/// </summary>
Pixel4 = 4,
/// <summary>
/// 8 bits per pixel. Each pixel consists of 1 byte.
/// </summary>
@ -28,4 +33,4 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
Pixel32 = 32
}
}
}

98
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -51,6 +51,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
private const int ColorPaletteSize8Bit = 1024;
/// <summary>
/// The color palette for an 4 bit image will have 16 entry's with 4 bytes for each entry.
/// </summary>
private const int ColorPaletteSize4Bit = 64;
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
@ -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<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
var quantizedColorBytes = quantizedColors.Length * 4;
PixelOperations<TPixel>.Instance.ToBgra32(this.configuration, quantizedColors, MemoryMarshal.Cast<byte, Bgra32>(colorPalette.Slice(0, quantizedColorBytes)));
Span<uint> colorPaletteAsUInt = MemoryMarshal.Cast<byte, uint>(colorPalette);
for (int i = 0; i < colorPaletteAsUInt.Length; i++)
{
colorPaletteAsUInt[i] = colorPaletteAsUInt[i] & 0x00FFFFFF; // Padding byte, always 0.
}
stream.Write(colorPalette);
ReadOnlySpan<TPixel> 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
}
}
}
/// <summary>
/// Writes an 4 Bit color image with a color palette. The color palette has 16 entry's with 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write4BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
{
MaxColors = 16
});
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize4Bit, AllocationOptions.Clean);
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);
ReadOnlySpan<byte> 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);
}
}
}
/// <summary>
/// Writes the color palette to the stream. The color palette has 4 bytes for each entry.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="quantizedColorPalette">The color palette from the quantized image.</param>
/// <param name="colorPalette">A temporary byte span to write the color palette to.</param>
private void WriteColorPalette<TPixel>(Stream stream, ReadOnlySpan<TPixel> quantizedColorPalette, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
int quantizedColorBytes = quantizedColorPalette.Length * 4;
PixelOperations<TPixel>.Instance.ToBgra32(this.configuration, quantizedColorPalette, MemoryMarshal.Cast<byte, Bgra32>(colorPalette.Slice(0, quantizedColorBytes)));
Span<uint> colorPaletteAsUInt = MemoryMarshal.Cast<byte, uint>(colorPalette);
for (int i = 0; i < colorPaletteAsUInt.Length; i++)
{
colorPaletteAsUInt[i] = colorPaletteAsUInt[i] & 0x00FFFFFF; // Padding byte, always 0.
}
stream.Write(colorPalette);
}
}
}

16
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<string, BmpBitsPerPixel> BmpBitsPerPixelFiles =
new TheoryData<string, BmpBitsPerPixel>
{
{ 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<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false);
[Theory]
[WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)]
public void Encode_4Bit_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
[Theory]
[WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)]
public void Encode_8BitGray_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)

Loading…
Cancel
Save