Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
public enum BmpBitsPerPixel : short
{
/// <summary>
/// 1 bit per pixel.
/// </summary>
Pixel1 = 1,

/// <summary>
/// 4 bits per pixel.
/// </summary>
Expand Down
11 changes: 1 addition & 10 deletions src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1303,16 +1303,7 @@ private void ReadInfoHeader()
short bitsPerPixel = this.infoHeader.BitsPerPixel;
this.bmpMetadata = this.metadata.GetBmpMetadata();
this.bmpMetadata.InfoHeaderType = infoHeaderType;

// We can only encode at these bit rates so far (1 bit per pixel is still missing).
if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel4)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel8)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel16)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32))
{
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
}
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Bmp/BmpEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public sealed class BmpEncoder : IImageEncoder, IBmpEncoderOptions

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

Expand Down
91 changes: 85 additions & 6 deletions src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// </summary>
private const int ColorPaletteSize4Bit = 64;

/// <summary>
/// The color palette for an 1 bit image will have 2 entry's with 4 bytes for each entry.
/// </summary>
private const int ColorPaletteSize1Bit = 8;

/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
Expand All @@ -79,7 +84,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
private readonly bool writeV4Header;

/// <summary>
/// The quantizer for reducing the color count for 8-Bit images.
/// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images.
/// </summary>
private readonly IQuantizer quantizer;

Expand All @@ -93,7 +98,7 @@ public BmpEncoderCore(IBmpEncoderOptions options, MemoryAllocator memoryAllocato
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel;
this.writeV4Header = options.SupportTransparency;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.quantizer = options.Quantizer ?? KnownQuantizers.Wu;
}

/// <summary>
Expand Down Expand Up @@ -180,6 +185,10 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
{
colorPaletteSize = ColorPaletteSize4Bit;
}
else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1)
{
colorPaletteSize = ColorPaletteSize1Bit;
}

var fileHeader = new BmpFileHeader(
type: BmpConstants.TypeMarkers.Bitmap,
Expand Down Expand Up @@ -241,6 +250,10 @@ private void WriteImage<TPixel>(Stream stream, ImageFrame<TPixel> image)
case BmpBitsPerPixel.Pixel4:
this.Write4BitColor(stream, image);
break;

case BmpBitsPerPixel.Pixel1:
this.Write1BitColor(stream, image);
break;
}
}

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

/// <summary>
/// Writes an 8 Bit image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
/// Writes an 8 bit image with a color palette. The color palette has 256 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>
Expand All @@ -349,7 +362,7 @@ private void Write8Bit<TPixel>(Stream stream, ImageFrame<TPixel> image)
}

/// <summary>
/// Writes an 8 Bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
/// Writes an 8 bit color image with a color palette. The color palette has 256 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>
Expand Down Expand Up @@ -377,7 +390,7 @@ private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Spa
}

/// <summary>
/// Writes an 8 Bit gray image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
/// Writes an 8 bit gray image with a color palette. The color palette has 256 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>
Expand Down Expand Up @@ -415,7 +428,7 @@ private void Write8BitGray<TPixel>(Stream stream, ImageFrame<TPixel> image, Span
}

/// <summary>
/// Writes an 4 Bit color image with a color palette. The color palette has 16 entry's with 4 bytes for each entry.
/// 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>
Expand Down Expand Up @@ -458,6 +471,52 @@ private void Write4BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image)
}
}

/// <summary>
/// Writes a 1 bit image with a color palette. The color palette has 2 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 Write1BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions()
{
MaxColors = 2
});
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(image, image.Bounds());
using IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize1Bit, AllocationOptions.Clean);

Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette);

ReadOnlySpan<byte> quantizedPixelRow = quantized.GetPixelRowSpan(0);
int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding;
for (int y = image.Height - 1; y >= 0; y--)
{
quantizedPixelRow = quantized.GetPixelRowSpan(y);

int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8;
for (int i = 0; i < endIdx; i += 8)
{
Write1BitPalette(stream, i, i + 8, quantizedPixelRow);
}

if (quantizedPixelRow.Length % 8 != 0)
{
int startIdx = quantizedPixelRow.Length - 7;
endIdx = quantizedPixelRow.Length;
Write1BitPalette(stream, startIdx, endIdx, quantizedPixelRow);
}

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>
Expand All @@ -478,5 +537,25 @@ private void WriteColorPalette<TPixel>(Stream stream, ReadOnlySpan<TPixel> quant

stream.Write(colorPalette);
}

/// <summary>
/// Writes a 1-bit palette.
/// </summary>
/// <param name="stream">The stream to write the palette to.</param>
/// <param name="startIdx">The start index.</param>
/// <param name="endIdx">The end index.</param>
/// <param name="quantizedPixelRow">A quantized pixel row.</param>
private static void Write1BitPalette(Stream stream, int startIdx, int endIdx, ReadOnlySpan<byte> quantizedPixelRow)
{
int shift = 7;
byte indices = 0;
for (int j = startIdx; j < endIdx; j++)
{
indices = (byte)(indices | ((byte)(quantizedPixelRow[j] & 1) << shift));
shift--;
}

stream.WriteByte(indices);
}
}
}
6 changes: 3 additions & 3 deletions src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using SixLabors.ImageSharp.Processing.Processors.Quantization;
Expand All @@ -24,8 +24,8 @@ internal interface IBmpEncoderOptions
bool SupportTransparency { get; }

/// <summary>
/// Gets the quantizer for reducing the color count for 8-Bit images.
/// Gets the quantizer for reducing the color count for 8-Bit, 4-Bit, and 1-Bit images.
/// </summary>
IQuantizer Quantizer { get; }
}
}
}
39 changes: 37 additions & 2 deletions tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class BmpEncoderTests
public static readonly TheoryData<string, BmpBitsPerPixel> BmpBitsPerPixelFiles =
new TheoryData<string, BmpBitsPerPixel>
{
{ Bit1, BmpBitsPerPixel.Pixel1 },
{ Bit4, BmpBitsPerPixel.Pixel4 },
{ Bit8, BmpBitsPerPixel.Pixel8 },
{ Rgb16, BmpBitsPerPixel.Pixel16 },
Expand Down Expand Up @@ -201,6 +202,34 @@ public void Encode_4Bit_WithV4Header_Works<TPixel>(
}
}

[Theory]
[WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)]
public void Encode_1Bit_WithV3Header_Works<TPixel>(
TestImageProvider<TPixel> provider,
BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
// The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows.
if (TestEnvironment.IsWindows)
{
TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false);
}
}

[Theory]
[WithFile(Bit1, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel1)]
public void Encode_1Bit_WithV4Header_Works<TPixel>(
TestImageProvider<TPixel> provider,
BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
// The Magick Reference Decoder can not decode 1-Bit bitmaps, so only execute this on windows.
if (TestEnvironment.IsWindows)
{
TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true);
}
}

[Theory]
[WithFile(Bit8Gs, PixelTypes.L8, BmpBitsPerPixel.Pixel8)]
public void Encode_8BitGray_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
Expand Down Expand Up @@ -297,7 +326,8 @@ public void Encode_WorksWithDiscontiguousBuffers<TPixel>(TestImageProvider<TPixe
private static void TestBmpEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
BmpBitsPerPixel bitsPerPixel,
bool supportTransparency = true,
bool supportTransparency = true, // if set to true, will write a V4 header, otherwise a V3 header.
IQuantizer quantizer = null,
ImageComparer customComparer = null)
where TPixel : unmanaged, IPixel<TPixel>
{
Expand All @@ -309,7 +339,12 @@ private static void TestBmpEncoderCore<TPixel>(
image.Mutate(c => c.MakeOpaque());
}

var encoder = new BmpEncoder { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency };
var encoder = new BmpEncoder
{
BitsPerPixel = bitsPerPixel,
SupportTransparency = supportTransparency,
Quantizer = quantizer ?? KnownQuantizers.Wu
};

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