From 1996831667dd1190e6fa82ef416b06db899623b0 Mon Sep 17 00:00:00 2001
From: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Date: Fri, 18 Jan 2019 08:11:55 +0100
Subject: [PATCH] Decoding Bitmaps with BITFIELDS masks (#796)
* decoding bitmaps with Bitfields masks
* added testcases for Bitfields bitmaps
* added parsing of the complete bitmap V4 header to get the color masks infos for the BITFIELDS compression
* writing now explicitly a Bitamp v3 header (40 bytes)
* added test image for issue #735
* fixed rescaling 5-bit / 6-bit to 0 - 255 range
* BitmapEncoder now writes BMP v4 header
* using MemoryMarshal.Cast to simplify parsing v4 header
* added testcases for bitmap v3, v4, v5
* Bitmap encoder writes again V3 header instead of V4. Additional fields for V4 are zero anyway.
* added parsing of special case for 56 bytes headers
* using the alpha mask in ReadRgb32BitFields() when its present
* added support for bitmasks greater then 8 bits per channel
* using MagickReferenceDecoder reference decoder for the test with 10 bits pixel masks
* changed bitmap constants to hex
* added enum for the bitmap info header type
* added support for 52 bytes header (same as 56 bytes without the alpha mask)
* clarified comment with difference between imagesharp and magick decoder for Rgba321010102
* BmpEncoder now writes a V4 info header and uses BITFIELDS compression
* workaround for issue that the decoder does not decode the alpha channel correctly: For V3 bitmaps, the alpha channel will be ignored during encoding
* Fix #732
---
src/ImageSharp/Formats/Bmp/BmpCompression.cs | 2 -
src/ImageSharp/Formats/Bmp/BmpConstants.cs | 36 ++
src/ImageSharp/Formats/Bmp/BmpDecoder.cs | 2 -
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 399 ++++++++++++++++--
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 50 ++-
src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 252 ++++++++++-
.../Formats/Bmp/BmpInfoHeaderType.cs | 48 +++
src/ImageSharp/Formats/Bmp/BmpMetaData.cs | 11 +-
.../Formats/Bmp/BmpDecoderTests.cs | 107 ++++-
tests/ImageSharp.Tests/TestImages.cs | 29 +-
tests/Images/Input/Bmp/issue735.bmp | Bin 0 -> 1334298 bytes
tests/Images/Input/Bmp/pal8v4.bmp | Bin 0 -> 9322 bytes
tests/Images/Input/Bmp/pal8v5.bmp | Bin 0 -> 9338 bytes
tests/Images/Input/Bmp/rgb16-565.bmp | Bin 0 -> 16450 bytes
tests/Images/Input/Bmp/rgb16-565pal.bmp | Bin 0 -> 17474 bytes
tests/Images/Input/Bmp/rgb16bfdef.bmp | Bin 0 -> 16450 bytes
tests/Images/Input/Bmp/rgb24.bmp | Bin 0 -> 24630 bytes
tests/Images/Input/Bmp/rgb32bf.bmp | Bin 0 -> 32578 bytes
tests/Images/Input/Bmp/rgb32bfdef.bmp | Bin 0 -> 32578 bytes
tests/Images/Input/Bmp/rgba32-1010102.bmp | Bin 0 -> 32650 bytes
tests/Images/Input/Bmp/rgba32.bmp | Bin 0 -> 32650 bytes
tests/Images/Input/Bmp/rgba32h56.bmp | Bin 0 -> 32582 bytes
22 files changed, 859 insertions(+), 77 deletions(-)
create mode 100644 src/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs
create mode 100644 tests/Images/Input/Bmp/issue735.bmp
create mode 100644 tests/Images/Input/Bmp/pal8v4.bmp
create mode 100644 tests/Images/Input/Bmp/pal8v5.bmp
create mode 100644 tests/Images/Input/Bmp/rgb16-565.bmp
create mode 100644 tests/Images/Input/Bmp/rgb16-565pal.bmp
create mode 100644 tests/Images/Input/Bmp/rgb16bfdef.bmp
create mode 100644 tests/Images/Input/Bmp/rgb24.bmp
create mode 100644 tests/Images/Input/Bmp/rgb32bf.bmp
create mode 100644 tests/Images/Input/Bmp/rgb32bfdef.bmp
create mode 100644 tests/Images/Input/Bmp/rgba32-1010102.bmp
create mode 100644 tests/Images/Input/Bmp/rgba32.bmp
create mode 100644 tests/Images/Input/Bmp/rgba32h56.bmp
diff --git a/src/ImageSharp/Formats/Bmp/BmpCompression.cs b/src/ImageSharp/Formats/Bmp/BmpCompression.cs
index ef063f0106..5f14d22436 100644
--- a/src/ImageSharp/Formats/Bmp/BmpCompression.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpCompression.cs
@@ -25,7 +25,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// If the first byte is zero, the record has different meanings, depending
/// on the second byte. If the second byte is zero, it is the end of the row,
/// if it is one, it is the end of the image.
- /// Not supported at the moment.
///
RLE8 = 1,
@@ -42,7 +41,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Each image row has a multiple of four elements. If the
/// row has less elements, zeros will be added at the right side.
- /// Not supported at the moment.
///
BitFields = 3,
diff --git a/src/ImageSharp/Formats/Bmp/BmpConstants.cs b/src/ImageSharp/Formats/Bmp/BmpConstants.cs
index 99799b619c..5cbed4af2b 100644
--- a/src/ImageSharp/Formats/Bmp/BmpConstants.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpConstants.cs
@@ -19,5 +19,41 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// The list of file extensions that equate to a bmp.
///
public static readonly IEnumerable FileExtensions = new[] { "bm", "bmp", "dip" };
+
+ ///
+ /// Valid magic bytes markers identifying a Bitmap file.
+ ///
+ internal static class TypeMarkers
+ {
+ ///
+ /// Single-image BMP file that may have been created under Windows or OS/2.
+ ///
+ public const int Bitmap = 0x4D42;
+
+ ///
+ /// OS/2 Bitmap Array.
+ ///
+ public const int BitmapArray = 0x4142;
+
+ ///
+ /// OS/2 Color Icon.
+ ///
+ public const int ColorIcon = 0x4943;
+
+ ///
+ /// OS/2 Color Pointer.
+ ///
+ public const int ColorPointer = 0x5043;
+
+ ///
+ /// OS/2 Icon.
+ ///
+ public const int Icon = 0x4349;
+
+ ///
+ /// OS/2 Pointer.
+ ///
+ public const int Pointer = 0x5450;
+ }
}
}
\ No newline at end of file
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs
index 3d079cf619..82af2a671e 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs
@@ -15,8 +15,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// - JPG
/// - PNG
/// - RLE4
- /// - RLE8
- /// - BitFields
///
/// Formats will be supported in a later releases. We advise always
/// to use only 24 Bit Windows bitmaps.
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 68528edcd6..362fe64430 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Buffers;
using System.Buffers.Binary;
using System.IO;
+using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
@@ -14,7 +16,7 @@ using SixLabors.Memory;
namespace SixLabors.ImageSharp.Formats.Bmp
{
///
- /// Performs the bmp decoding operation.
+ /// Performs the bitmap decoding operation.
///
///
/// A useful decoding source example can be found at
@@ -22,19 +24,19 @@ namespace SixLabors.ImageSharp.Formats.Bmp
internal sealed class BmpDecoderCore
{
///
- /// The mask for the red part of the color for 16 bit rgb bitmaps.
+ /// The default mask for the red part of the color for 16 bit rgb bitmaps.
///
- private const int Rgb16RMask = 0x7C00;
+ private const int DefaultRgb16RMask = 0x7C00;
///
- /// The mask for the green part of the color for 16 bit rgb bitmaps.
+ /// The default mask for the green part of the color for 16 bit rgb bitmaps.
///
- private const int Rgb16GMask = 0x3E0;
+ private const int DefaultRgb16GMask = 0x3E0;
///
- /// The mask for the blue part of the color for 16 bit rgb bitmaps.
+ /// The default mask for the blue part of the color for 16 bit rgb bitmaps.
///
- private const int Rgb16BMask = 0x1F;
+ private const int DefaultRgb16BMask = 0x1F;
///
/// RLE8 flag value that indicates following byte has special meaning.
@@ -62,10 +64,15 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private Stream stream;
///
- /// The metadata
+ /// The metadata.
///
private ImageMetaData metaData;
+ ///
+ /// The bmp specific metadata.
+ ///
+ private BmpMetaData bmpMetaData;
+
///
/// The file header containing general information.
/// TODO: Why is this not used? We advance the stream but do not use the values parsed.
@@ -85,7 +92,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// Initializes a new instance of the class.
///
/// The configuration.
- /// The options
+ /// The options.
public BmpDecoderCore(Configuration configuration, IBmpDecoderOptions options)
{
this.configuration = configuration;
@@ -119,7 +126,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp
case BmpCompression.RGB:
if (this.infoHeader.BitsPerPixel == 32)
{
- this.ReadRgb32(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
+ if (this.bmpMetaData.InfoHeaderType == BmpInfoHeaderType.WinVersion3)
+ {
+ this.ReadRgb32Slow(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
+ }
+ else
+ {
+ this.ReadRgb32Fast(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
+ }
}
else if (this.infoHeader.BitsPerPixel == 24)
{
@@ -146,6 +160,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.ReadRle8(pixels, palette, this.infoHeader.Width, this.infoHeader.Height, inverted);
break;
+
+ case BmpCompression.BitFields:
+ this.ReadBitFields(pixels, inverted);
+
+ break;
+
default:
throw new NotSupportedException("Does not support this kind of bitmap files.");
}
@@ -199,12 +219,39 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
///
- /// Performs final shifting from a 5bit value to an 8bit one.
+ /// Decodes a bitmap containing BITFIELDS Compression type. For each color channel, there will be bitmask
+ /// which will be used to determine which bits belong to that channel.
///
- /// The masked and shifted value
- /// The
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2));
+ /// The pixel format.
+ /// The output pixel buffer containing the decoded image.
+ /// Whether the bitmap is inverted.
+ private void ReadBitFields(Buffer2D pixels, bool inverted)
+ where TPixel : struct, IPixel
+ {
+ if (this.infoHeader.BitsPerPixel == 16)
+ {
+ this.ReadRgb16(
+ pixels,
+ this.infoHeader.Width,
+ this.infoHeader.Height,
+ inverted,
+ this.infoHeader.RedMask,
+ this.infoHeader.GreenMask,
+ this.infoHeader.BlueMask);
+ }
+ else
+ {
+ this.ReadRgb32BitFields(
+ pixels,
+ this.infoHeader.Width,
+ this.infoHeader.Height,
+ inverted,
+ this.infoHeader.RedMask,
+ this.infoHeader.GreenMask,
+ this.infoHeader.BlueMask,
+ this.infoHeader.AlphaMask);
+ }
+ }
///
/// Looks up color values and builds the image from de-compressed RLE8 data.
@@ -240,12 +287,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
///
- /// Produce uncompressed bitmap data from RLE8 stream
+ /// Produce uncompressed bitmap data from RLE8 stream.
///
///
- /// RLE8 is a 2-byte run-length encoding
- ///
If first byte is 0, the second byte may have special meaning
- ///
Otherwise, first byte is the length of the run and second byte is the color for the run
+ /// RLE8 is a 2-byte run-length encoding.
+ ///
If first byte is 0, the second byte may have special meaning.
+ ///
Otherwise, first byte is the length of the run and second byte is the color for the run.
///
/// The width of the bitmap.
/// Buffer for uncompressed data.
@@ -382,20 +429,32 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
///
- /// Reads the 16 bit color palette from the stream
+ /// Reads the 16 bit color palette from the stream.
///
/// The pixel format.
/// The to assign the palette to.
/// The width of the bitmap.
/// The height of the bitmap.
/// Whether the bitmap is inverted.
- private void ReadRgb16(Buffer2D pixels, int width, int height, bool inverted)
+ /// The bitmask for the red channel.
+ /// The bitmask for the green channel.
+ /// The bitmask for the blue channel.
+ private void ReadRgb16(Buffer2D pixels, int width, int height, bool inverted, int redMask = DefaultRgb16RMask, int greenMask = DefaultRgb16GMask, int blueMask = DefaultRgb16BMask)
where TPixel : struct, IPixel
{
int padding = CalculatePadding(width, 2);
int stride = (width * 2) + padding;
TPixel color = default;
+ int rightShiftRedMask = CalculateRightShift((uint)redMask);
+ int rightShiftGreenMask = CalculateRightShift((uint)greenMask);
+ int rightShiftBlueMask = CalculateRightShift((uint)blueMask);
+
+ // Each color channel contains either 5 or 6 Bits values.
+ int redMaskBits = CountBits((uint)redMask);
+ int greenMaskBits = CountBits((uint)greenMask);
+ int blueMaskBits = CountBits((uint)blueMask);
+
using (IManagedByteBuffer buffer = this.memoryAllocator.AllocateManagedByteBuffer(stride))
{
for (int y = 0; y < height; y++)
@@ -409,10 +468,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
{
short temp = BitConverter.ToInt16(buffer.Array, offset);
- var rgb = new Rgb24(
- GetBytesFrom5BitValue((temp & Rgb16RMask) >> 10),
- GetBytesFrom5BitValue((temp & Rgb16GMask) >> 5),
- GetBytesFrom5BitValue(temp & Rgb16BMask));
+ // Rescale values, so the values range from 0 to 255.
+ int r = (redMaskBits == 5) ? GetBytesFrom5BitValue((temp & redMask) >> rightShiftRedMask) : GetBytesFrom6BitValue((temp & redMask) >> rightShiftRedMask);
+ int g = (greenMaskBits == 5) ? GetBytesFrom5BitValue((temp & greenMask) >> rightShiftGreenMask) : GetBytesFrom6BitValue((temp & greenMask) >> rightShiftGreenMask);
+ int b = (blueMaskBits == 5) ? GetBytesFrom5BitValue((temp & blueMask) >> rightShiftBlueMask) : GetBytesFrom6BitValue((temp & blueMask) >> rightShiftBlueMask);
+ var rgb = new Rgb24((byte)r, (byte)g, (byte)b);
color.FromRgb24(rgb);
pixelRow[x] = color;
@@ -423,7 +483,23 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
///
- /// Reads the 24 bit color palette from the stream
+ /// Performs final shifting from a 5bit value to an 8bit one.
+ ///
+ /// The masked and shifted value.
+ /// The
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2));
+
+ ///
+ /// Performs final shifting from a 6bit value to an 8bit one.
+ ///
+ /// The masked and shifted value.
+ /// The
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static byte GetBytesFrom6BitValue(int value) => (byte)((value << 2) | (value >> 4));
+
+ ///
+ /// Reads the 24 bit color palette from the stream.
///
/// The pixel format.
/// The to assign the palette to.
@@ -452,14 +528,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
///
- /// Reads the 32 bit color palette from the stream
+ /// Reads the 32 bit color palette from the stream.
///
/// The pixel format.
/// The to assign the palette to.
/// The width of the bitmap.
/// The height of the bitmap.
/// Whether the bitmap is inverted.
- private void ReadRgb32(Buffer2D pixels, int width, int height, bool inverted)
+ private void ReadRgb32Fast(Buffer2D pixels, int width, int height, bool inverted)
where TPixel : struct, IPixel
{
int padding = CalculatePadding(width, 4);
@@ -480,6 +556,228 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
}
+ ///
+ /// Reads the 32 bit color palette from the stream, checking the alpha component of each pixel.
+ /// This is a special case only used for 32bpp WinBMPv3 files, which could be in either BGR0 or BGRA format.
+ ///
+ /// The pixel format.
+ /// The to assign the palette to.
+ /// The width of the bitmap.
+ /// The height of the bitmap.
+ /// Whether the bitmap is inverted.
+ private void ReadRgb32Slow(Buffer2D pixels, int width, int height, bool inverted)
+ where TPixel : struct, IPixel
+ {
+ int padding = CalculatePadding(width, 4);
+
+ using (IManagedByteBuffer row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, padding))
+ using (IMemoryOwner bgraRow = this.memoryAllocator.Allocate(width))
+ {
+ Span bgraRowSpan = bgraRow.GetSpan();
+ long currentPosition = this.stream.Position;
+ bool hasAlpha = false;
+
+ // Loop though the rows checking each pixel. We start by assuming it's
+ // an BGR0 image. If we hit a non-zero alpha value, then we know it's
+ // actually a BGRA image, and change tactics accordingly.
+ for (int y = 0; y < height; y++)
+ {
+ this.stream.Read(row);
+
+ PixelOperations.Instance.FromBgra32Bytes(
+ this.configuration,
+ row.GetSpan(),
+ bgraRowSpan,
+ width);
+
+ // Check each pixel in the row to see if it has an alpha value.
+ for (int x = 0; x < width; x++)
+ {
+ Bgra32 bgra = bgraRowSpan[x];
+ if (bgra.A > 0)
+ {
+ hasAlpha = true;
+ break;
+ }
+ }
+
+ if (hasAlpha)
+ {
+ break;
+ }
+ }
+
+ // Reset our stream for a second pass.
+ this.stream.Position = currentPosition;
+
+ // Process the pixels in bulk taking the raw alpha component value.
+ if (hasAlpha)
+ {
+ for (int y = 0; y < height; y++)
+ {
+ this.stream.Read(row);
+
+ int newY = Invert(y, height, inverted);
+ Span pixelSpan = pixels.GetRowSpan(newY);
+
+ PixelOperations.Instance.FromBgra32Bytes(
+ this.configuration,
+ row.GetSpan(),
+ pixelSpan,
+ width);
+ }
+
+ return;
+ }
+
+ // Slow path. We need to set each alpha component value to fully opaque.
+ for (int y = 0; y < height; y++)
+ {
+ this.stream.Read(row);
+ PixelOperations.Instance.FromBgra32Bytes(
+ this.configuration,
+ row.GetSpan(),
+ bgraRowSpan,
+ width);
+
+ int newY = Invert(y, height, inverted);
+ Span pixelSpan = pixels.GetRowSpan(newY);
+
+ for (int x = 0; x < width; x++)
+ {
+ Bgra32 bgra = bgraRowSpan[x];
+ bgra.A = byte.MaxValue;
+ ref TPixel pixel = ref pixelSpan[x];
+ pixel.FromBgra32(bgra);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Decode an 32 Bit Bitmap containing a bitmask for each color channel.
+ ///
+ /// The pixel format.
+ /// The output pixel buffer containing the decoded image.
+ /// The width of the image.
+ /// The height of the image.
+ /// Whether the bitmap is inverted.
+ /// The bitmask for the red channel.
+ /// The bitmask for the green channel.
+ /// The bitmask for the blue channel.
+ /// The bitmask for the alpha channel.
+ private void ReadRgb32BitFields(Buffer2D pixels, int width, int height, bool inverted, int redMask, int greenMask, int blueMask, int alphaMask)
+ where TPixel : struct, IPixel
+ {
+ TPixel color = default;
+ int padding = CalculatePadding(width, 4);
+ int stride = (width * 4) + padding;
+
+ int rightShiftRedMask = CalculateRightShift((uint)redMask);
+ int rightShiftGreenMask = CalculateRightShift((uint)greenMask);
+ int rightShiftBlueMask = CalculateRightShift((uint)blueMask);
+ int rightShiftAlphaMask = CalculateRightShift((uint)alphaMask);
+
+ int bitsRedMask = CountBits((uint)redMask);
+ int bitsGreenMask = CountBits((uint)greenMask);
+ int bitsBlueMask = CountBits((uint)blueMask);
+ int bitsAlphaMask = CountBits((uint)alphaMask);
+ float invMaxValueRed = 1.0f / (0xFFFFFFFF >> (32 - bitsRedMask));
+ float invMaxValueGreen = 1.0f / (0xFFFFFFFF >> (32 - bitsGreenMask));
+ float invMaxValueBlue = 1.0f / (0xFFFFFFFF >> (32 - bitsBlueMask));
+ uint maxValueAlpha = 0xFFFFFFFF >> (32 - bitsAlphaMask);
+ float invMaxValueAlpha = 1.0f / maxValueAlpha;
+
+ bool unusualBitMask = false;
+ if (bitsRedMask > 8 || bitsGreenMask > 8 || bitsBlueMask > 8 || invMaxValueAlpha > 8)
+ {
+ unusualBitMask = true;
+ }
+
+ using (IManagedByteBuffer buffer = this.memoryAllocator.AllocateManagedByteBuffer(stride))
+ {
+ for (int y = 0; y < height; y++)
+ {
+ this.stream.Read(buffer.Array, 0, stride);
+ int newY = Invert(y, height, inverted);
+ Span pixelRow = pixels.GetRowSpan(newY);
+
+ int offset = 0;
+ for (int x = 0; x < width; x++)
+ {
+ uint temp = BitConverter.ToUInt32(buffer.Array, offset);
+
+ if (unusualBitMask)
+ {
+ uint r = (uint)(temp & redMask) >> rightShiftRedMask;
+ uint g = (uint)(temp & greenMask) >> rightShiftGreenMask;
+ uint b = (uint)(temp & blueMask) >> rightShiftBlueMask;
+ float alpha = alphaMask != 0 ? invMaxValueAlpha * ((uint)(temp & alphaMask) >> rightShiftAlphaMask) : 1.0f;
+ var vector4 = new Vector4(
+ r * invMaxValueRed,
+ g * invMaxValueGreen,
+ b * invMaxValueBlue,
+ alpha);
+ color.FromVector4(vector4);
+ }
+ else
+ {
+ byte r = (byte)((temp & redMask) >> rightShiftRedMask);
+ byte g = (byte)((temp & greenMask) >> rightShiftGreenMask);
+ byte b = (byte)((temp & blueMask) >> rightShiftBlueMask);
+ byte a = alphaMask != 0 ? (byte)((temp & alphaMask) >> rightShiftAlphaMask) : (byte)255;
+ color.FromRgba32(new Rgba32(r, g, b, a));
+ }
+
+ pixelRow[x] = color;
+ offset += 4;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Calculates the necessary right shifts for a given color bitmask (the 0 bits to the right).
+ ///
+ /// The color bit mask.
+ /// Number of bits to shift right.
+ private static int CalculateRightShift(uint n)
+ {
+ int count = 0;
+ while (n > 0)
+ {
+ if ((1 & n) == 0)
+ {
+ count++;
+ }
+ else
+ {
+ break;
+ }
+
+ n >>= 1;
+ }
+
+ return count;
+ }
+
+ ///
+ /// Counts none zero bits.
+ ///
+ /// A color mask.
+ /// The none zero bits.
+ private static int CountBits(uint n)
+ {
+ int count = 0;
+ while (n != 0)
+ {
+ count++;
+ n &= n - 1;
+ }
+
+ return count;
+ }
+
///
/// Reads the from the stream.
///
@@ -508,20 +806,54 @@ namespace SixLabors.ImageSharp.Formats.Bmp
// read the rest of the header
this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize);
+ BmpInfoHeaderType inofHeaderType = BmpInfoHeaderType.WinVersion2;
if (headerSize == BmpInfoHeader.CoreSize)
{
// 12 bytes
+ inofHeaderType = BmpInfoHeaderType.WinVersion2;
this.infoHeader = BmpInfoHeader.ParseCore(buffer);
}
else if (headerSize == BmpInfoHeader.Os22ShortSize)
{
// 16 bytes
+ inofHeaderType = BmpInfoHeaderType.Os2Version2Short;
this.infoHeader = BmpInfoHeader.ParseOs22Short(buffer);
}
- else if (headerSize >= BmpInfoHeader.Size)
+ else if (headerSize == BmpInfoHeader.SizeV3)
+ {
+ // == 40 bytes
+ inofHeaderType = BmpInfoHeaderType.WinVersion3;
+ this.infoHeader = BmpInfoHeader.ParseV3(buffer);
+
+ // if the info header is BMP version 3 and the compression type is BITFIELDS,
+ // color masks for each color channel follow the info header.
+ if (this.infoHeader.Compression == BmpCompression.BitFields)
+ {
+ byte[] bitfieldsBuffer = new byte[12];
+ this.stream.Read(bitfieldsBuffer, 0, 12);
+ Span data = bitfieldsBuffer.AsSpan();
+ this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4));
+ this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
+ this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4));
+ }
+ }
+ else if (headerSize == BmpInfoHeader.AdobeV3Size)
+ {
+ // == 52 bytes
+ inofHeaderType = BmpInfoHeaderType.AdobeVersion3;
+ this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: false);
+ }
+ else if (headerSize == BmpInfoHeader.AdobeV3WithAlphaSize)
+ {
+ // == 56 bytes
+ inofHeaderType = BmpInfoHeaderType.AdobeVersion3WithAlpha;
+ this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: true);
+ }
+ else if (headerSize >= BmpInfoHeader.SizeV4)
{
- // >= 40 bytes
- this.infoHeader = BmpInfoHeader.Parse(buffer);
+ // >= 108 bytes
+ inofHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5;
+ this.infoHeader = BmpInfoHeader.ParseV4(buffer);
}
else
{
@@ -548,13 +880,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.metaData = meta;
short bitsPerPixel = this.infoHeader.BitsPerPixel;
- BmpMetaData bmpMetaData = this.metaData.GetFormatMetaData(BmpFormat.Instance);
+ this.bmpMetaData = this.metaData.GetFormatMetaData(BmpFormat.Instance);
+ this.bmpMetaData.InfoHeaderType = inofHeaderType;
// We can only encode at these bit rates so far.
if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32))
{
- bmpMetaData.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
+ this.bmpMetaData.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
}
// skip the remaining header because we can't read those parts
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index a67c581eb0..27a38bc0d1 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -23,6 +23,26 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
private int padding;
+ ///
+ /// The mask for the alpha channel of the color for a 32 bit rgba bitmaps.
+ ///
+ private const int Rgba32AlphaMask = 0xFF << 24;
+
+ ///
+ /// The mask for the red part of the color for a 32 bit rgba bitmaps.
+ ///
+ private const int Rgba32RedMask = 0xFF << 16;
+
+ ///
+ /// The mask for the green part of the color for a 32 bit rgba bitmaps.
+ ///
+ private const int Rgba32GreenMask = 0xFF << 8;
+
+ ///
+ /// The mask for the blue part of the color for a 32 bit rgba bitmaps.
+ ///
+ private const int Rgba32BlueMask = 0xFF;
+
private readonly MemoryAllocator memoryAllocator;
private Configuration configuration;
@@ -92,8 +112,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
}
+ int infoHeaderSize = BmpInfoHeader.SizeV4;
var infoHeader = new BmpInfoHeader(
- headerSize: BmpInfoHeader.Size,
+ headerSize: infoHeaderSize,
height: image.Height,
width: image.Width,
bitsPerPixel: bpp,
@@ -102,26 +123,37 @@ namespace SixLabors.ImageSharp.Formats.Bmp
clrUsed: 0,
clrImportant: 0,
xPelsPerMeter: hResolution,
- yPelsPerMeter: vResolution);
+ yPelsPerMeter: vResolution)
+ {
+ RedMask = Rgba32RedMask,
+ GreenMask = Rgba32GreenMask,
+ BlueMask = Rgba32BlueMask,
+ Compression = BmpCompression.BitFields
+ };
+
+ if (this.bitsPerPixel == BmpBitsPerPixel.Pixel32)
+ {
+ infoHeader.AlphaMask = Rgba32AlphaMask;
+ }
var fileHeader = new BmpFileHeader(
- type: 19778, // BM
- fileSize: 54 + infoHeader.ImageSize,
+ type: BmpConstants.TypeMarkers.Bitmap,
+ fileSize: BmpFileHeader.Size + infoHeaderSize + infoHeader.ImageSize,
reserved: 0,
- offset: 54);
+ offset: BmpFileHeader.Size + infoHeaderSize);
#if NETCOREAPP2_1
- Span buffer = stackalloc byte[40];
+ Span buffer = stackalloc byte[infoHeaderSize];
#else
- byte[] buffer = new byte[40];
+ byte[] buffer = new byte[infoHeaderSize];
#endif
fileHeader.WriteTo(buffer);
stream.Write(buffer, 0, BmpFileHeader.Size);
- infoHeader.WriteTo(buffer);
+ infoHeader.WriteV4Header(buffer);
- stream.Write(buffer, 0, 40);
+ stream.Write(buffer, 0, infoHeaderSize);
this.WriteImage(stream, image.Frames.RootFrame);
diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
index 5177bc325c..316df4acc9 100644
--- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
@@ -16,11 +16,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct BmpInfoHeader
{
- ///
- /// Defines the size of the BITMAPINFOHEADER data structure in the bitmap file.
- ///
- public const int Size = 40;
-
///
/// Defines the size of the BITMAPCOREHEADER data structure in the bitmap file.
///
@@ -31,10 +26,30 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
public const int Os22ShortSize = 16;
+ ///
+ /// Defines the size of the BITMAPINFOHEADER (BMP Version 3) data structure in the bitmap file.
+ ///
+ public const int SizeV3 = 40;
+
+ ///
+ /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it.
+ ///
+ public const int AdobeV3Size = 52;
+
+ ///
+ /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks (including the alpha channel) are part of the info header instead of following it.
+ ///
+ public const int AdobeV3WithAlphaSize = 56;
+
+ ///
+ /// Defines the size of the BITMAPINFOHEADER (BMP Version 4) data structure in the bitmap file.
+ ///
+ public const int SizeV4 = 108;
+
///
/// Defines the size of the biggest supported header data structure in the bitmap file.
///
- public const int MaxHeaderSize = Size;
+ public const int MaxHeaderSize = SizeV4;
///
/// Defines the size of the field.
@@ -52,7 +67,24 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int xPelsPerMeter = 0,
int yPelsPerMeter = 0,
int clrUsed = 0,
- int clrImportant = 0)
+ int clrImportant = 0,
+ int redMask = 0,
+ int greenMask = 0,
+ int blueMask = 0,
+ int alphaMask = 0,
+ int csType = 0,
+ int redX = 0,
+ int redY = 0,
+ int redZ = 0,
+ int greenX = 0,
+ int greenY = 0,
+ int greenZ = 0,
+ int blueX = 0,
+ int blueY = 0,
+ int blueZ = 0,
+ int gammeRed = 0,
+ int gammeGreen = 0,
+ int gammeBlue = 0)
{
this.HeaderSize = headerSize;
this.Width = width;
@@ -65,6 +97,23 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.YPelsPerMeter = yPelsPerMeter;
this.ClrUsed = clrUsed;
this.ClrImportant = clrImportant;
+ this.RedMask = redMask;
+ this.GreenMask = greenMask;
+ this.BlueMask = blueMask;
+ this.AlphaMask = alphaMask;
+ this.CsType = csType;
+ this.RedX = redX;
+ this.RedY = redY;
+ this.RedZ = redZ;
+ this.GreenX = greenX;
+ this.GreenY = greenY;
+ this.GreenZ = greenZ;
+ this.BlueX = blueX;
+ this.BlueY = blueY;
+ this.BlueZ = blueZ;
+ this.GammaRed = gammeRed;
+ this.GammaGreen = gammeGreen;
+ this.GammaBlue = gammeBlue;
}
///
@@ -130,23 +179,92 @@ namespace SixLabors.ImageSharp.Formats.Bmp
public int ClrImportant { get; set; }
///
- /// Parses the full BITMAPINFOHEADER header (40 bytes).
+ /// Gets or sets red color mask. This is used with the BITFIELDS decoding.
///
- /// The data to parse.
- /// Parsed header
- ///
- public static BmpInfoHeader Parse(ReadOnlySpan data)
- {
- if (data.Length != Size)
- {
- throw new ArgumentException(nameof(data), $"Must be {Size} bytes. Was {data.Length} bytes.");
- }
+ public int RedMask { get; set; }
- return MemoryMarshal.Cast(data)[0];
- }
+ ///
+ /// Gets or sets green color mask. This is used with the BITFIELDS decoding.
+ ///
+ public int GreenMask { get; set; }
+
+ ///
+ /// Gets or sets blue color mask. This is used with the BITFIELDS decoding.
+ ///
+ public int BlueMask { get; set; }
+
+ ///
+ /// Gets or sets alpha color mask. This is not used yet.
+ ///
+ public int AlphaMask { get; set; }
+
+ ///
+ /// Gets or sets the Color space type. Not used yet.
+ ///
+ public int CsType { get; set; }
+
+ ///
+ /// Gets or sets the X coordinate of red endpoint. Not used yet.
+ ///
+ public int RedX { get; set; }
+
+ ///
+ /// Gets or sets the Y coordinate of red endpoint. Not used yet.
+ ///
+ public int RedY { get; set; }
+
+ ///
+ /// Gets or sets the Z coordinate of red endpoint. Not used yet.
+ ///
+ public int RedZ { get; set; }
+
+ ///
+ /// Gets or sets the X coordinate of green endpoint. Not used yet.
+ ///
+ public int GreenX { get; set; }
+
+ ///
+ /// Gets or sets the Y coordinate of green endpoint. Not used yet.
+ ///
+ public int GreenY { get; set; }
+
+ ///
+ /// Gets or sets the Z coordinate of green endpoint. Not used yet.
+ ///
+ public int GreenZ { get; set; }
+
+ ///
+ /// Gets or sets the X coordinate of blue endpoint. Not used yet.
+ ///
+ public int BlueX { get; set; }
///
- /// Parses the BITMAPCOREHEADER consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes).
+ /// Gets or sets the Y coordinate of blue endpoint. Not used yet.
+ ///
+ public int BlueY { get; set; }
+
+ ///
+ /// Gets or sets the Z coordinate of blue endpoint. Not used yet.
+ ///
+ public int BlueZ { get; set; }
+
+ ///
+ /// Gets or sets the Gamma red coordinate scale value. Not used yet.
+ ///
+ public int GammaRed { get; set; }
+
+ ///
+ /// Gets or sets the Gamma green coordinate scale value. Not used yet.
+ ///
+ public int GammaGreen { get; set; }
+
+ ///
+ /// Gets or sets the Gamma blue coordinate scale value. Not used yet.
+ ///
+ public int GammaBlue { get; set; }
+
+ ///
+ /// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes).
///
/// The data to parse.
/// Parsed header
@@ -163,7 +281,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height
- /// are 4 bytes instead of 2.
+ /// are 4 bytes instead of 2, resulting in 16 bytes total.
///
/// The data to parse.
/// Parsed header
@@ -178,7 +296,97 @@ namespace SixLabors.ImageSharp.Formats.Bmp
bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)));
}
- public unsafe void WriteTo(Span buffer)
+ ///
+ /// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes).
+ ///
+ /// The data to parse.
+ /// The parsed header.
+ ///
+ public static BmpInfoHeader ParseV3(ReadOnlySpan data)
+ {
+ return new BmpInfoHeader(
+ headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
+ width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
+ height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
+ planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)),
+ bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)),
+ compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)),
+ imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)),
+ xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)),
+ yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)),
+ clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)),
+ clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)));
+ }
+
+ ///
+ /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it.
+ /// 52 bytes without the alpha mask, 56 bytes with the alpha mask.
+ ///
+ /// The data to parse.
+ /// Indicates, if the alpha bitmask is present.
+ /// The parsed header.
+ ///
+ public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true)
+ {
+ return new BmpInfoHeader(
+ headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
+ width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
+ height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
+ planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)),
+ bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)),
+ compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)),
+ imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)),
+ xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)),
+ yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)),
+ clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)),
+ clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)),
+ redMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(40, 4)),
+ greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)),
+ blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)),
+ alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0);
+ }
+
+ ///
+ /// Parses the full BMP Version 4 BITMAPINFOHEADER header (108 bytes).
+ ///
+ /// The data to parse.
+ /// The parsed header.
+ ///
+ public static BmpInfoHeader ParseV4(ReadOnlySpan data)
+ {
+ if (data.Length != SizeV4)
+ {
+ throw new ArgumentException(nameof(data), $"Must be {SizeV4} bytes. Was {data.Length} bytes.");
+ }
+
+ return MemoryMarshal.Cast(data)[0];
+ }
+
+ ///
+ /// Writes a bitmap version 3 (Microsoft Windows NT) header to a buffer (40 bytes).
+ ///
+ /// The buffer to write to.
+ public void WriteV3Header(Span buffer)
+ {
+ buffer.Clear();
+ BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(0, 4), SizeV3);
+ BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(4, 4), this.Width);
+ BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(8, 4), this.Height);
+ BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(12, 2), this.Planes);
+ BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(14, 2), this.BitsPerPixel);
+ BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(16, 4), (int)this.Compression);
+ BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(20, 4), this.ImageSize);
+ BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(24, 4), this.XPelsPerMeter);
+ BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(28, 4), this.YPelsPerMeter);
+ BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(32, 4), this.ClrUsed);
+ BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(36, 4), this.ClrImportant);
+ }
+
+ ///
+ /// Writes a complete Bitmap V4 header to a buffer.
+ ///
+ /// The buffer to write to.
+ public unsafe void WriteV4Header(Span buffer)
{
ref BmpInfoHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer));
diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs
new file mode 100644
index 0000000000..a4ce0115f9
--- /dev/null
+++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeaderType.cs
@@ -0,0 +1,48 @@
+namespace SixLabors.ImageSharp.Formats.Bmp
+{
+ ///
+ /// Enum value for the different bitmap info header types. The enum value is the number of bytes for the specific bitmap header.
+ ///
+ public enum BmpInfoHeaderType
+ {
+ ///
+ /// Bitmap Core or BMP Version 2 header (Microsoft Windows 2.x).
+ ///
+ WinVersion2 = 12,
+
+ ///
+ /// Short variant of the OS/2 Version 2 bitmap header.
+ ///
+ Os2Version2Short = 16,
+
+ ///
+ /// BMP Version 3 header (Microsoft Windows 3.x or Microsoft Windows NT).
+ ///
+ WinVersion3 = 40,
+
+ ///
+ /// Adobe variant of the BMP Version 3 header.
+ ///
+ AdobeVersion3 = 52,
+
+ ///
+ /// Adobe variant of the BMP Version 3 header with an alpha mask.
+ ///
+ AdobeVersion3WithAlpha = 56,
+
+ ///
+ /// BMP Version 2.x header (IBM OS/2 2.x).
+ ///
+ Os2Version2 = 64,
+
+ ///
+ /// BMP Version 4 header (Microsoft Windows 95).
+ ///
+ WinVersion4 = 108,
+
+ ///
+ /// BMP Version 5 header (Windows NT 5.0, 98 or later).
+ ///
+ WinVersion5 = 124,
+ }
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs
index 8b33e30fa6..2d86856177 100644
--- a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs
@@ -19,7 +19,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// Initializes a new instance of the class.
///
/// The metadata to create an instance from.
- private BmpMetaData(BmpMetaData other) => this.BitsPerPixel = other.BitsPerPixel;
+ private BmpMetaData(BmpMetaData other)
+ {
+ this.BitsPerPixel = other.BitsPerPixel;
+ this.InfoHeaderType = other.InfoHeaderType;
+ }
+
+ ///
+ /// Gets or sets the bitmap info header type.
+ ///
+ public BmpInfoHeaderType InfoHeaderType { get; set; }
///
/// Gets or sets the number of bits per pixel.
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index 251475567f..60e45ae35f 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
@@ -4,6 +4,9 @@
using System.IO;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
+
using Xunit;
// ReSharper disable InconsistentNaming
@@ -19,6 +22,8 @@ namespace SixLabors.ImageSharp.Tests
public static readonly string[] AllBmpFiles = All;
+ public static readonly string[] BitfieldsBmpFiles = BitFields;
+
public static readonly TheoryData RatioFiles =
new TheoryData
{
@@ -34,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests
{
using (Image image = provider.GetImage(new BmpDecoder()))
{
- image.DebugSave(provider, "bmp");
+ image.DebugSave(provider);
if (TestEnvironment.IsWindows)
{
@@ -44,17 +49,47 @@ namespace SixLabors.ImageSharp.Tests
}
[Theory]
- [WithFile(F, CommonNonDefaultPixelTypes)]
- public void BmpDecoder_IsNotBoundToSinglePixelType(TestImageProvider provider)
+ [WithFileCollection(nameof(BitfieldsBmpFiles), PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecodeBitfields(TestImageProvider provider)
where TPixel : struct, IPixel
{
using (Image image = provider.GetImage(new BmpDecoder()))
{
- image.DebugSave(provider, "bmp");
+ image.DebugSave(provider);
image.CompareToOriginal(provider);
}
}
+ [Theory]
+ [WithFile(Bit32Rgba, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecodeBitmap_WithAlphaChannel(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, new MagickReferenceDecoder());
+ }
+ }
+
+ [Theory]
+ [WithFile(Rgba321010102, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecodeBitfields_WithUnusualBitmasks(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+
+ // Choosing large tolerance of 6.1 here, because for some reason with the MagickReferenceDecoder the alpha channel
+ // seems to be wrong. This bitmap has an alpha channel of two bits. In many cases this alpha channel has a value of 3,
+ // which should be remapped to 255 for RGBA32, but the magick decoder has a value of 191 set.
+ // The total difference without the alpha channel is still: 0.0204%
+ // Exporting the image as PNG with GIMP yields to the same result as the imagesharp implementation.
+ image.CompareToOriginal(provider, ImageComparer.TolerantPercentage(6.1f), new MagickReferenceDecoder());
+ }
+ }
+
[Theory]
[WithFile(WinBmpv2, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeBmpv2(TestImageProvider provider)
@@ -62,7 +97,67 @@ namespace SixLabors.ImageSharp.Tests
{
using (Image image = provider.GetImage(new BmpDecoder()))
{
- image.DebugSave(provider, "png");
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(WinBmpv3, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecodeBmpv3(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(Rgba32bf56, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecodeAdobeBmpv3(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, new MagickReferenceDecoder());
+ }
+ }
+
+ [Theory]
+ [WithFile(WinBmpv4, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecodeBmpv4(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(WinBmpv5, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecodeBmpv5(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(F, CommonNonDefaultPixelTypes)]
+ public void BmpDecoder_IsNotBoundToSinglePixelType(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
image.CompareToOriginal(provider);
}
}
@@ -74,7 +169,7 @@ namespace SixLabors.ImageSharp.Tests
{
using (Image image = provider.GetImage(new BmpDecoder()))
{
- image.DebugSave(provider, "png");
+ image.DebugSave(provider);
image.CompareToOriginal(provider);
}
}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 8d8a32fba3..5ecc266572 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -205,12 +205,36 @@ namespace SixLabors.ImageSharp.Tests
public const string Bit16 = "Bmp/test16.bmp";
public const string Bit16Inverted = "Bmp/test16-inverted.bmp";
public const string Bit32Rgb = "Bmp/rgb32.bmp";
-
+ public const string Bit32Rgba = "Bmp/rgba32.bmp";
+
// Note: This format can be called OS/2 BMPv1, or Windows BMPv2
public const string WinBmpv2 = "Bmp/pal8os2v1_winv2.bmp";
+ public const string WinBmpv3 = "Bmp/rgb24.bmp";
+ public const string WinBmpv4 = "Bmp/pal8v4.bmp";
+ public const string WinBmpv5 = "Bmp/pal8v5.bmp";
public const string Bit8Palette4 = "Bmp/pal8-0.bmp";
public const string Os2v2Short = "Bmp/pal8os2v2-16.bmp";
+ // Bitmap images with compression type BITFIELDS
+ public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp";
+ public const string Rgb32bf = "Bmp/rgb32bf.bmp";
+ public const string Rgb16565 = "Bmp/rgb16-565.bmp";
+ public const string Rgb16bfdef = "Bmp/rgb16bfdef.bmp";
+ public const string Rgb16565pal = "Bmp/rgb16-565pal.bmp";
+ public const string Issue735 = "Bmp/issue735.bmp";
+ public const string Rgba32bf56 = "Bmp/rgba32h56.bmp";
+ public const string Rgba321010102 = "Bmp/rgba32-1010102.bmp";
+
+ public static readonly string[] BitFields
+ = {
+ Rgb32bfdef,
+ Rgb32bf,
+ Rgb16565,
+ Rgb16bfdef,
+ Rgb16565pal,
+ Issue735,
+ };
+
public static readonly string[] All
= {
Car,
@@ -225,7 +249,8 @@ namespace SixLabors.ImageSharp.Tests
Bit8,
Bit8Inverted,
Bit16,
- Bit16Inverted
+ Bit16Inverted,
+ Bit32Rgb
};
}
diff --git a/tests/Images/Input/Bmp/issue735.bmp b/tests/Images/Input/Bmp/issue735.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..31fadfcf5ad3513de36517603b39c2ce618d9fe2
GIT binary patch
literal 1334298
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