Skip to content

Commit ea8bef4

Browse files
committed
Add support for encoding 1 bit per pixel bitmaps
1 parent 6fa903c commit ea8bef4

File tree

5 files changed

+139
-20
lines changed

5 files changed

+139
-20
lines changed

src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
88
/// </summary>
99
public enum BmpBitsPerPixel : short
1010
{
11+
/// <summary>
12+
/// 1 bit per pixel.
13+
/// </summary>
14+
Pixel1 = 1,
15+
1116
/// <summary>
1217
/// 4 bits per pixel.
1318
/// </summary>

src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,16 +1303,7 @@ private void ReadInfoHeader()
13031303
short bitsPerPixel = this.infoHeader.BitsPerPixel;
13041304
this.bmpMetadata = this.metadata.GetBmpMetadata();
13051305
this.bmpMetadata.InfoHeaderType = infoHeaderType;
1306-
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)
1310-
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel16)
1311-
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24)
1312-
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32))
1313-
{
1314-
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
1315-
}
1306+
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
13161307
}
13171308

13181309
/// <summary>

src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
5656
/// </summary>
5757
private const int ColorPaletteSize4Bit = 64;
5858

59+
/// <summary>
60+
/// The color palette for an 1 bit image will have 2 entry's with 4 bytes for each entry.
61+
/// </summary>
62+
private const int ColorPaletteSize1Bit = 8;
63+
5964
/// <summary>
6065
/// Used for allocating memory during processing operations.
6166
/// </summary>
@@ -79,7 +84,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
7984
private readonly bool writeV4Header;
8085

8186
/// <summary>
82-
/// The quantizer for reducing the color count for 8-Bit images.
87+
/// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images.
8388
/// </summary>
8489
private readonly IQuantizer quantizer;
8590

@@ -180,6 +185,10 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
180185
{
181186
colorPaletteSize = ColorPaletteSize4Bit;
182187
}
188+
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1)
189+
{
190+
colorPaletteSize = ColorPaletteSize1Bit;
191+
}
183192

184193
var fileHeader = new BmpFileHeader(
185194
type: BmpConstants.TypeMarkers.Bitmap,
@@ -241,6 +250,10 @@ private void WriteImage<TPixel>(Stream stream, ImageFrame<TPixel> image)
241250
case BmpBitsPerPixel.Pixel4:
242251
this.Write4BitColor(stream, image);
243252
break;
253+
254+
case BmpBitsPerPixel.Pixel1:
255+
this.Write1BitColor(stream, image);
256+
break;
244257
}
245258
}
246259

@@ -325,7 +338,7 @@ private void Write16Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
325338
}
326339

327340
/// <summary>
328-
/// Writes an 8 Bit image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
341+
/// Writes an 8 bit image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
329342
/// </summary>
330343
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
331344
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
@@ -349,7 +362,7 @@ private void Write8Bit<TPixel>(Stream stream, ImageFrame<TPixel> image)
349362
}
350363

351364
/// <summary>
352-
/// Writes an 8 Bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
365+
/// Writes an 8 bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
353366
/// </summary>
354367
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
355368
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
@@ -377,7 +390,7 @@ private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Spa
377390
}
378391

379392
/// <summary>
380-
/// Writes an 8 Bit gray image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
393+
/// Writes an 8 bit gray image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
381394
/// </summary>
382395
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
383396
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
@@ -415,7 +428,7 @@ private void Write8BitGray<TPixel>(Stream stream, ImageFrame<TPixel> image, Span
415428
}
416429

417430
/// <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.
431+
/// Writes an 4 bit color image with a color palette. The color palette has 16 entry's with 4 bytes for each entry.
419432
/// </summary>
420433
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
421434
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
@@ -458,6 +471,52 @@ private void Write4BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image)
458471
}
459472
}
460473

474+
/// <summary>
475+
/// Writes a 1 bit image with a color palette. The color palette has 2 entry's with 4 bytes for each entry.
476+
/// </summary>
477+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
478+
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
479+
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
480+
private void Write1BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image)
481+
where TPixel : unmanaged, IPixel<TPixel>
482+
{
483+
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
484+
{
485+
MaxColors = 2
486+
});
487+
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
488+
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize1Bit, AllocationOptions.Clean);
489+
490+
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
491+
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
492+
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);
493+
494+
ReadOnlySpan<byte> quantizedPixelRow = quantized.GetPixelRowSpan(0);
495+
int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding;
496+
for (int y = image.Height - 1; y >= 0; y--)
497+
{
498+
quantizedPixelRow = quantized.GetPixelRowSpan(y);
499+
500+
int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8;
501+
for (int i = 0; i < endIdx; i += 8)
502+
{
503+
Write1BitPalette(stream, i, i + 8, quantizedPixelRow);
504+
}
505+
506+
if (quantizedPixelRow.Length % 8 != 0)
507+
{
508+
int startIdx = quantizedPixelRow.Length - 7;
509+
endIdx = quantizedPixelRow.Length;
510+
Write1BitPalette(stream, startIdx, endIdx, quantizedPixelRow);
511+
}
512+
513+
for (int i = 0; i < rowPadding; i++)
514+
{
515+
stream.WriteByte(0);
516+
}
517+
}
518+
}
519+
461520
/// <summary>
462521
/// Writes the color palette to the stream. The color palette has 4 bytes for each entry.
463522
/// </summary>
@@ -478,5 +537,25 @@ private void WriteColorPalette<TPixel>(Stream stream, ReadOnlySpan<TPixel> quant
478537

479538
stream.Write(colorPalette);
480539
}
540+
541+
/// <summary>
542+
/// Writes a 1-bit palette.
543+
/// </summary>
544+
/// <param name="stream">The stream to write the palette to.</param>
545+
/// <param name="startIdx">The start index.</param>
546+
/// <param name="endIdx">The end index.</param>
547+
/// <param name="quantizedPixelRow">A quantized pixel row.</param>
548+
private static void Write1BitPalette(Stream stream, int startIdx, int endIdx, ReadOnlySpan<byte> quantizedPixelRow)
549+
{
550+
int shift = 7;
551+
byte indices = 0;
552+
for (int j = startIdx; j < endIdx; j++)
553+
{
554+
indices = (byte)(indices | ((byte)(quantizedPixelRow[j] & 1) << shift));
555+
shift--;
556+
}
557+
558+
stream.WriteByte(indices);
559+
}
481560
}
482561
}

src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs

Lines changed: 4 additions & 3 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
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -24,8 +24,9 @@ internal interface IBmpEncoderOptions
2424
bool SupportTransparency { get; }
2525

2626
/// <summary>
27-
/// Gets the quantizer for reducing the color count for 8-Bit images.
27+
/// Gets the quantizer for reducing the color count for 8-Bit, 4-Bit, and 1-Bit images.
28+
/// Defaults to the Octree Quantizer.
2829
/// </summary>
2930
IQuantizer Quantizer { get; }
3031
}
31-
}
32+
}

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

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class BmpEncoderTests
4040
public static readonly TheoryData<string, BmpBitsPerPixel> BmpBitsPerPixelFiles =
4141
new TheoryData<string, BmpBitsPerPixel>
4242
{
43+
{ Bit1, BmpBitsPerPixel.Pixel1 },
4344
{ Bit4, BmpBitsPerPixel.Pixel4 },
4445
{ Bit8, BmpBitsPerPixel.Pixel8 },
4546
{ Rgb16, BmpBitsPerPixel.Pixel16 },
@@ -201,6 +202,42 @@ public void Encode_4Bit_WithV4Header_Works<TPixel>(
201202
}
202203
}
203204

205+
[Theory]
206+
[WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)]
207+
public void Encode_1Bit_WithV3Header_Works<TPixel>(
208+
TestImageProvider<TPixel> provider,
209+
BmpBitsPerPixel bitsPerPixel)
210+
where TPixel : unmanaged, IPixel<TPixel>
211+
{
212+
// The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows.
213+
if (TestEnvironment.IsWindows)
214+
{
215+
TestBmpEncoderCore(
216+
provider,
217+
bitsPerPixel,
218+
supportTransparency: false,
219+
quantizer: KnownQuantizers.Wu); // using the wu quantizer, because the octree seems to have problems with just two colors.
220+
}
221+
}
222+
223+
[Theory]
224+
[WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)]
225+
public void Encode_1Bit_WithV4Header_Works<TPixel>(
226+
TestImageProvider<TPixel> provider,
227+
BmpBitsPerPixel bitsPerPixel)
228+
where TPixel : unmanaged, IPixel<TPixel>
229+
{
230+
// The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows.
231+
if (TestEnvironment.IsWindows)
232+
{
233+
TestBmpEncoderCore(
234+
provider,
235+
bitsPerPixel,
236+
supportTransparency: true,
237+
quantizer: KnownQuantizers.Wu); // using the wu quantizer, because the octree seems to have problems with just two colors.
238+
}
239+
}
240+
204241
[Theory]
205242
[WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)]
206243
public void Encode_8BitGray_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
@@ -297,7 +334,8 @@ public void Encode_WorksWithDiscontiguousBuffers<TPixel>(TestImageProvider<TPixe
297334
private static void TestBmpEncoderCore<TPixel>(
298335
TestImageProvider<TPixel> provider,
299336
BmpBitsPerPixel bitsPerPixel,
300-
bool supportTransparency = true,
337+
bool supportTransparency = true, // if set to true, will write a V4 header, otherwise a V3 header.
338+
IQuantizer quantizer = null,
301339
ImageComparer customComparer = null)
302340
where TPixel : unmanaged, IPixel<TPixel>
303341
{
@@ -309,7 +347,12 @@ private static void TestBmpEncoderCore<TPixel>(
309347
image.Mutate(c => c.MakeOpaque());
310348
}
311349

312-
var encoder = new BmpEncoder { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency };
350+
var encoder = new BmpEncoder
351+
{
352+
BitsPerPixel = bitsPerPixel,
353+
SupportTransparency = supportTransparency,
354+
Quantizer = quantizer ?? KnownQuantizers.Octree
355+
};
313356

314357
// Does DebugSave & load reference CompareToReferenceInput():
315358
image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder, customComparer);

0 commit comments

Comments
 (0)