Skip to content

Commit a8cb711

Browse files
Merge pull request #1623 from SixLabors/bp/bmp1bit
Add support for encoding 1 bit per pixel bitmaps
2 parents 6fa903c + 33192d3 commit a8cb711

File tree

6 files changed

+132
-22
lines changed

6 files changed

+132
-22
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/BmpEncoder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public sealed class BmpEncoder : IImageEncoder, IBmpEncoderOptions
3030

3131
/// <summary>
3232
/// Gets or sets the quantizer for reducing the color count for 8-Bit images.
33-
/// Defaults to OctreeQuantizer.
33+
/// Defaults to Wu Quantizer.
3434
/// </summary>
3535
public IQuantizer Quantizer { get; set; }
3636

src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

Lines changed: 85 additions & 6 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

@@ -93,7 +98,7 @@ public BmpEncoderCore(IBmpEncoderOptions options, MemoryAllocator memoryAllocato
9398
this.memoryAllocator = memoryAllocator;
9499
this.bitsPerPixel = options.BitsPerPixel;
95100
this.writeV4Header = options.SupportTransparency;
96-
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
101+
this.quantizer = options.Quantizer ?? KnownQuantizers.Wu;
97102
}
98103

99104
/// <summary>
@@ -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: 3 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,8 @@ 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.
2828
/// </summary>
2929
IQuantizer Quantizer { get; }
3030
}
31-
}
31+
}

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

Lines changed: 37 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,34 @@ 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(provider, bitsPerPixel, supportTransparency: false);
216+
}
217+
}
218+
219+
[Theory]
220+
[WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)]
221+
public void Encode_1Bit_WithV4Header_Works<TPixel>(
222+
TestImageProvider<TPixel> provider,
223+
BmpBitsPerPixel bitsPerPixel)
224+
where TPixel : unmanaged, IPixel<TPixel>
225+
{
226+
// The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows.
227+
if (TestEnvironment.IsWindows)
228+
{
229+
TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
230+
}
231+
}
232+
204233
[Theory]
205234
[WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)]
206235
public void Encode_8BitGray_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
@@ -297,7 +326,8 @@ public void Encode_WorksWithDiscontiguousBuffers<TPixel>(TestImageProvider<TPixe
297326
private static void TestBmpEncoderCore<TPixel>(
298327
TestImageProvider<TPixel> provider,
299328
BmpBitsPerPixel bitsPerPixel,
300-
bool supportTransparency = true,
329+
bool supportTransparency = true, // if set to true, will write a V4 header, otherwise a V3 header.
330+
IQuantizer quantizer = null,
301331
ImageComparer customComparer = null)
302332
where TPixel : unmanaged, IPixel<TPixel>
303333
{
@@ -309,7 +339,12 @@ private static void TestBmpEncoderCore<TPixel>(
309339
image.Mutate(c => c.MakeOpaque());
310340
}
311341

312-
var encoder = new BmpEncoder { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency };
342+
var encoder = new BmpEncoder
343+
{
344+
BitsPerPixel = bitsPerPixel,
345+
SupportTransparency = supportTransparency,
346+
Quantizer = quantizer ?? KnownQuantizers.Wu
347+
};
313348

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

0 commit comments

Comments
 (0)