Skip to content

Commit 6fa903c

Browse files
Merge pull request #1622 from SixLabors/bp/bmp4bit
Add support for encoding 4 bit per pixel bitmaps
2 parents a6c1785 + 60bd394 commit 6fa903c

File tree

4 files changed

+127
-21
lines changed

4 files changed

+127
-21
lines changed

src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

44
namespace SixLabors.ImageSharp.Formats.Bmp
@@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
88
/// </summary>
99
public enum BmpBitsPerPixel : short
1010
{
11+
/// <summary>
12+
/// 4 bits per pixel.
13+
/// </summary>
14+
Pixel4 = 4,
15+
1116
/// <summary>
1217
/// 8 bits per pixel. Each pixel consists of 1 byte.
1318
/// </summary>
@@ -28,4 +33,4 @@ public enum BmpBitsPerPixel : short
2833
/// </summary>
2934
Pixel32 = 32
3035
}
31-
}
36+
}

src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,8 +1304,9 @@ private void ReadInfoHeader()
13041304
this.bmpMetadata = this.metadata.GetBmpMetadata();
13051305
this.bmpMetadata.InfoHeaderType = infoHeaderType;
13061306

1307-
// We can only encode at these bit rates so far (1 bit and 4 bit are still missing).
1308-
if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel8)
1307+
// We can only encode at these bit rates so far (1 bit per pixel is still missing).
1308+
if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel4)
1309+
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel8)
13091310
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel16)
13101311
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24)
13111312
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32))

src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
5151
/// </summary>
5252
private const int ColorPaletteSize8Bit = 1024;
5353

54+
/// <summary>
55+
/// The color palette for an 4 bit image will have 16 entry's with 4 bytes for each entry.
56+
/// </summary>
57+
private const int ColorPaletteSize4Bit = 64;
58+
5459
/// <summary>
5560
/// Used for allocating memory during processing operations.
5661
/// </summary>
@@ -107,7 +112,7 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
107112
this.configuration = image.GetConfiguration();
108113
ImageMetadata metadata = image.Metadata;
109114
BmpMetadata bmpMetadata = metadata.GetBmpMetadata();
110-
this.bitsPerPixel = this.bitsPerPixel ?? bmpMetadata.BitsPerPixel;
115+
this.bitsPerPixel ??= bmpMetadata.BitsPerPixel;
111116

112117
short bpp = (short)this.bitsPerPixel;
113118
int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32);
@@ -166,7 +171,15 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
166171
infoHeader.Compression = BmpCompression.BitFields;
167172
}
168173

169-
int colorPaletteSize = this.bitsPerPixel == BmpBitsPerPixel.Pixel8 ? ColorPaletteSize8Bit : 0;
174+
int colorPaletteSize = 0;
175+
if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8)
176+
{
177+
colorPaletteSize = ColorPaletteSize8Bit;
178+
}
179+
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4)
180+
{
181+
colorPaletteSize = ColorPaletteSize4Bit;
182+
}
170183

171184
var fileHeader = new BmpFileHeader(
172185
type: BmpConstants.TypeMarkers.Bitmap,
@@ -224,6 +237,10 @@ private void WriteImage<TPixel>(Stream stream, ImageFrame<TPixel> image)
224237
case BmpBitsPerPixel.Pixel8:
225238
this.Write8Bit(stream, image);
226239
break;
240+
241+
case BmpBitsPerPixel.Pixel4:
242+
this.Write4BitColor(stream, image);
243+
break;
227244
}
228245
}
229246

@@ -344,16 +361,8 @@ private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Spa
344361
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration);
345362
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
346363

347-
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
348-
var quantizedColorBytes = quantizedColors.Length * 4;
349-
PixelOperations<TPixel>.Instance.ToBgra32(this.configuration, quantizedColors, MemoryMarshal.Cast<byte, Bgra32>(colorPalette.Slice(0, quantizedColorBytes)));
350-
Span<uint> colorPaletteAsUInt = MemoryMarshal.Cast<byte, uint>(colorPalette);
351-
for (int i = 0; i < colorPaletteAsUInt.Length; i++)
352-
{
353-
colorPaletteAsUInt[i] = colorPaletteAsUInt[i] & 0x00FFFFFF; // Padding byte, always 0.
354-
}
355-
356-
stream.Write(colorPalette);
364+
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
365+
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);
357366

358367
for (int y = image.Height - 1; y >= 0; y--)
359368
{
@@ -404,5 +413,70 @@ private void Write8BitGray<TPixel>(Stream stream, ImageFrame<TPixel> image, Span
404413
}
405414
}
406415
}
416+
417+
/// <summary>
418+
/// Writes an 4 Bit color image with a color palette. The color palette has 16 entry's with 4 bytes for each entry.
419+
/// </summary>
420+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
421+
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
422+
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
423+
private void Write4BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image)
424+
where TPixel : unmanaged, IPixel<TPixel>
425+
{
426+
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
427+
{
428+
MaxColors = 16
429+
});
430+
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
431+
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize4Bit, AllocationOptions.Clean);
432+
433+
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
434+
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
435+
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);
436+
437+
ReadOnlySpan<byte> pixelRowSpan = quantized.GetPixelRowSpan(0);
438+
int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding;
439+
for (int y = image.Height - 1; y >= 0; y--)
440+
{
441+
pixelRowSpan = quantized.GetPixelRowSpan(y);
442+
443+
int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1;
444+
for (int i = 0; i < endIdx; i += 2)
445+
{
446+
stream.WriteByte((byte)((pixelRowSpan[i] << 4) | pixelRowSpan[i + 1]));
447+
}
448+
449+
if (pixelRowSpan.Length % 2 != 0)
450+
{
451+
stream.WriteByte((byte)((pixelRowSpan[pixelRowSpan.Length - 1] << 4) | 0));
452+
}
453+
454+
for (int i = 0; i < rowPadding; i++)
455+
{
456+
stream.WriteByte(0);
457+
}
458+
}
459+
}
460+
461+
/// <summary>
462+
/// Writes the color palette to the stream. The color palette has 4 bytes for each entry.
463+
/// </summary>
464+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
465+
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
466+
/// <param name="quantizedColorPalette">The color palette from the quantized image.</param>
467+
/// <param name="colorPalette">A temporary byte span to write the color palette to.</param>
468+
private void WriteColorPalette<TPixel>(Stream stream, ReadOnlySpan<TPixel> quantizedColorPalette, Span<byte> colorPalette)
469+
where TPixel : unmanaged, IPixel<TPixel>
470+
{
471+
int quantizedColorBytes = quantizedColorPalette.Length * 4;
472+
PixelOperations<TPixel>.Instance.ToBgra32(this.configuration, quantizedColorPalette, MemoryMarshal.Cast<byte, Bgra32>(colorPalette.Slice(0, quantizedColorBytes)));
473+
Span<uint> colorPaletteAsUInt = MemoryMarshal.Cast<byte, uint>(colorPalette);
474+
for (int i = 0; i < colorPaletteAsUInt.Length; i++)
475+
{
476+
colorPaletteAsUInt[i] = colorPaletteAsUInt[i] & 0x00FFFFFF; // Padding byte, always 0.
477+
}
478+
479+
stream.Write(colorPalette);
480+
}
407481
}
408482
}

tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
1414

1515
using Xunit;
16-
using Xunit.Abstractions;
1716

1817
using static SixLabors.ImageSharp.Tests.TestImages.Bmp;
1918

@@ -41,14 +40,13 @@ public class BmpEncoderTests
4140
public static readonly TheoryData<string, BmpBitsPerPixel> BmpBitsPerPixelFiles =
4241
new TheoryData<string, BmpBitsPerPixel>
4342
{
43+
{ Bit4, BmpBitsPerPixel.Pixel4 },
44+
{ Bit8, BmpBitsPerPixel.Pixel8 },
45+
{ Rgb16, BmpBitsPerPixel.Pixel16 },
4446
{ Car, BmpBitsPerPixel.Pixel24 },
4547
{ Bit32Rgb, BmpBitsPerPixel.Pixel32 }
4648
};
4749

48-
public BmpEncoderTests(ITestOutputHelper output) => this.Output = output;
49-
50-
private ITestOutputHelper Output { get; }
51-
5250
[Theory]
5351
[MemberData(nameof(RatioFiles))]
5452
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
@@ -175,6 +173,34 @@ public void Encode_8BitGray_WithV3Header_Works<TPixel>(TestImageProvider<TPixel>
175173
bitsPerPixel,
176174
supportTransparency: false);
177175

176+
[Theory]
177+
[WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)]
178+
public void Encode_4Bit_WithV3Header_Works<TPixel>(
179+
TestImageProvider<TPixel> provider,
180+
BmpBitsPerPixel bitsPerPixel)
181+
where TPixel : unmanaged, IPixel<TPixel>
182+
{
183+
// The Magick Reference Decoder can not decode 4-Bit bitmaps, so only execute this on windows.
184+
if (TestEnvironment.IsWindows)
185+
{
186+
TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false);
187+
}
188+
}
189+
190+
[Theory]
191+
[WithFile(Bit4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel4)]
192+
public void Encode_4Bit_WithV4Header_Works<TPixel>(
193+
TestImageProvider<TPixel> provider,
194+
BmpBitsPerPixel bitsPerPixel)
195+
where TPixel : unmanaged, IPixel<TPixel>
196+
{
197+
// The Magick Reference Decoder can not decode 4-Bit bitmaps, so only execute this on windows.
198+
if (TestEnvironment.IsWindows)
199+
{
200+
TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
201+
}
202+
}
203+
178204
[Theory]
179205
[WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)]
180206
public void Encode_8BitGray_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)

0 commit comments

Comments
 (0)