diff --git a/src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs b/src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs
new file mode 100644
index 0000000000..e8afb422a8
--- /dev/null
+++ b/src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Bmp
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ internal readonly struct BmpArrayFileHeader
+ {
+ public BmpArrayFileHeader(short type, int size, int offsetToNext, short width, short height)
+ {
+ this.Type = type;
+ this.Size = size;
+ this.OffsetToNext = offsetToNext;
+ this.ScreenWidth = width;
+ this.ScreenHeight = height;
+ }
+
+ ///
+ /// Gets the Bitmap identifier.
+ /// The field used to identify the bitmap file: 0x42 0x41 (Hex code points for B and A).
+ ///
+ public short Type { get; }
+
+ ///
+ /// Gets the size of this header.
+ ///
+ public int Size { get; }
+
+ ///
+ /// Gets the offset to next OS2BMPARRAYFILEHEADER.
+ /// This offset is calculated from the starting byte of the file. A value of zero indicates that this header is for the last image in the array list.
+ ///
+ public int OffsetToNext { get; }
+
+ ///
+ /// Gets the width of the image display in pixels.
+ ///
+ public short ScreenWidth { get; }
+
+ ///
+ /// Gets the height of the image display in pixels.
+ ///
+ public short ScreenHeight { get; }
+
+ public static BmpArrayFileHeader Parse(Span data)
+ {
+ return MemoryMarshal.Cast(data)[0];
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 294b49ed7e..1ceb35283a 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -1053,18 +1053,11 @@ private void ReadInfoHeader()
this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize);
int headerSize = BinaryPrimitives.ReadInt32LittleEndian(buffer);
- if (headerSize < BmpInfoHeader.CoreSize)
+ if (headerSize < BmpInfoHeader.CoreSize || headerSize > BmpInfoHeader.MaxHeaderSize)
{
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize is '{headerSize}'.");
}
- int skipAmount = 0;
- if (headerSize > BmpInfoHeader.MaxHeaderSize)
- {
- skipAmount = headerSize - BmpInfoHeader.MaxHeaderSize;
- headerSize = BmpInfoHeader.MaxHeaderSize;
- }
-
// Read the rest of the header.
this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize);
@@ -1169,15 +1162,13 @@ private void ReadInfoHeader()
{
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
}
-
- // Skip the remaining header because we can't read those parts.
- this.stream.Skip(skipAmount);
}
///
/// Reads the from the stream.
///
- private void ReadFileHeader()
+ /// The color map size in bytes, if it could be determined by the file header. Otherwise -1.
+ private int ReadFileHeader()
{
#if NETCOREAPP2_1
Span buffer = stackalloc byte[BmpFileHeader.Size];
@@ -1186,12 +1177,36 @@ private void ReadFileHeader()
#endif
this.stream.Read(buffer, 0, BmpFileHeader.Size);
- this.fileHeader = BmpFileHeader.Parse(buffer);
-
- if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
+ short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(buffer);
+ switch (fileTypeMarker)
{
- BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{this.fileHeader.Type}'.");
+ case BmpConstants.TypeMarkers.Bitmap:
+ this.fileHeader = BmpFileHeader.Parse(buffer);
+ break;
+ case BmpConstants.TypeMarkers.BitmapArray:
+ // The Array file header is followed by the bitmap file header of the first image.
+ var arrayHeader = BmpArrayFileHeader.Parse(buffer);
+ this.stream.Read(buffer, 0, BmpFileHeader.Size);
+ this.fileHeader = BmpFileHeader.Parse(buffer);
+ if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
+ {
+ BmpThrowHelper.ThrowNotSupportedException($"Unsupported bitmap file inside a BitmapArray file. File header bitmap type marker '{this.fileHeader.Type}'.");
+ }
+
+ if (arrayHeader.OffsetToNext != 0)
+ {
+ int colorMapSizeBytes = arrayHeader.OffsetToNext - arrayHeader.Size;
+ return colorMapSizeBytes;
+ }
+
+ break;
+
+ default:
+ BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{fileTypeMarker}'.");
+ break;
}
+
+ return -1;
}
///
@@ -1203,7 +1218,7 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett
{
this.stream = stream;
- this.ReadFileHeader();
+ int colorMapSizeBytes = this.ReadFileHeader();
this.ReadInfoHeader();
// see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517
@@ -1218,7 +1233,6 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett
this.infoHeader.Height = -this.infoHeader.Height;
}
- int colorMapSize = -1;
int bytesPerColorMapEntry = 4;
if (this.infoHeader.ClrUsed == 0)
@@ -1227,35 +1241,38 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett
|| this.infoHeader.BitsPerPixel == 4
|| this.infoHeader.BitsPerPixel == 8)
{
- int colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
+ if (colorMapSizeBytes == -1)
+ {
+ colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
+ }
+
int colorCountForBitDepth = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel);
bytesPerColorMapEntry = colorMapSizeBytes / colorCountForBitDepth;
// Edge case for less-than-full-sized palette: bytesPerColorMapEntry should be at least 3.
bytesPerColorMapEntry = Math.Max(bytesPerColorMapEntry, 3);
- colorMapSize = colorMapSizeBytes;
}
}
else
{
- colorMapSize = this.infoHeader.ClrUsed * bytesPerColorMapEntry;
+ colorMapSizeBytes = this.infoHeader.ClrUsed * bytesPerColorMapEntry;
}
palette = null;
- if (colorMapSize > 0)
+ if (colorMapSizeBytes > 0)
{
// Usually the color palette is 1024 byte (256 colors * 4), but the documentation does not mention a size limit.
// Make sure, that we will not read pass the bitmap offset (starting position of image data).
- if ((this.stream.Position + colorMapSize) > this.fileHeader.Offset)
+ if ((this.stream.Position + colorMapSizeBytes) > this.fileHeader.Offset)
{
BmpThrowHelper.ThrowImageFormatException(
- $"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSize}' is invalid or the bitmap offset.");
+ $"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset.");
}
- palette = new byte[colorMapSize];
+ palette = new byte[colorMapSizeBytes];
- this.stream.Read(palette, 0, colorMapSize);
+ this.stream.Read(palette, 0, colorMapSizeBytes);
}
this.infoHeader.VerifyDimensions();
diff --git a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs
index e39a2af0e4..661275fc90 100644
--- a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
internal readonly struct BmpFileHeader
{
///
- /// Defines of the data structure in the bitmap file.
+ /// Defines the size of the data structure in the bitmap file.
///
public const int Size = 14;
@@ -69,4 +69,4 @@ public unsafe void WriteTo(Span buffer)
dest = this;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
index 4f862d9295..3d7510bc2a 100644
--- a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@@ -22,7 +22,9 @@ public IImageFormat DetectFormat(ReadOnlySpan header)
private bool IsSupportedFileFormat(ReadOnlySpan header)
{
- return header.Length >= this.HeaderSize && BinaryPrimitives.ReadInt16LittleEndian(header) == BmpConstants.TypeMarkers.Bitmap;
+ short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(header);
+ return header.Length >= this.HeaderSize &&
+ (fileTypeMarker == BmpConstants.TypeMarkers.Bitmap || fileTypeMarker == BmpConstants.TypeMarkers.BitmapArray);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
index 6da5f73e3f..ca90020d85 100644
--- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
@@ -51,10 +51,15 @@ internal struct BmpInfoHeader
///
public const int SizeV4 = 108;
+ ///
+ /// Defines the size of the BITMAPINFOHEADER (BMP Version 5) data structure in the bitmap file.
+ ///
+ public const int SizeV5 = 124;
+
///
/// Defines the size of the biggest supported header data structure in the bitmap file.
///
- public const int MaxHeaderSize = SizeV4;
+ public const int MaxHeaderSize = SizeV5;
///
/// Defines the size of the field.
@@ -272,7 +277,7 @@ public BmpInfoHeader(
/// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes).
///
/// The data to parse.
- /// Parsed header
+ /// The parsed header.
///
public static BmpInfoHeader ParseCore(ReadOnlySpan data)
{
@@ -289,7 +294,7 @@ public static BmpInfoHeader ParseCore(ReadOnlySpan data)
/// are 4 bytes instead of 2, resulting in 16 bytes total.
///
/// The data to parse.
- /// Parsed header
+ /// The parsed header.
///
public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data)
{
@@ -406,7 +411,7 @@ public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan data)
///
public static BmpInfoHeader ParseV4(ReadOnlySpan data)
{
- if (data.Length != SizeV4)
+ if (data.Length < SizeV4)
{
throw new ArgumentException(nameof(data), $"Must be {SizeV4} bytes. Was {data.Length} bytes.");
}
@@ -457,4 +462,4 @@ internal void VerifyDimensions()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index c4dfa724cc..a95703609b 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
@@ -343,7 +343,6 @@ public void BmpDecoder_ThrowsNotSupportedException_OnUnsupportedBitmaps(
}
[Theory]
- [WithFile(Rgba32bf56AdobeV3, PixelTypes.Rgba32)]
[WithFile(Rgb32h52AdobeV3, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeAdobeBmpv3(TestImageProvider provider)
where TPixel : struct, IPixel
@@ -355,6 +354,18 @@ public void BmpDecoder_CanDecodeAdobeBmpv3(TestImageProvider pro
}
}
+ [Theory]
+ [WithFile(Rgba32bf56AdobeV3, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecodeAdobeBmpv3_WithAlpha(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)
@@ -429,12 +440,35 @@ public void BmpDecoder_CanDecode4BytePerEntryPalette(TestImageProvider(TestImageProvider(TestImageProvider p
// image.CompareToOriginal(provider, new MagickReferenceDecoder());
}
}
+
+ [Theory]
+ [WithFile(Os2BitmapArray9s, PixelTypes.Rgba32)]
+ [WithFile(Os2BitmapArrayDiamond, PixelTypes.Rgba32)]
+ [WithFile(Os2BitmapArraySkater, PixelTypes.Rgba32)]
+ [WithFile(Os2BitmapArraySpade, PixelTypes.Rgba32)]
+ [WithFile(Os2BitmapArraySunflower, PixelTypes.Rgba32)]
+ [WithFile(Os2BitmapArrayMarble, PixelTypes.Rgba32)]
+ [WithFile(Os2BitmapArrayWarpd, PixelTypes.Rgba32)]
+ [WithFile(Os2BitmapArrayPines, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_Os2BitmapArray(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+
+ // TODO: Neither System.Drawing or MagickReferenceDecoder can correctly decode this file.
+ // image.CompareToOriginal(provider);
+ }
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
index 178e652ae7..7412f70a11 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
@@ -14,6 +14,8 @@
using Xunit;
using Xunit.Abstractions;
+// ReSharper disable InconsistentNaming
+
namespace SixLabors.ImageSharp.Tests.Formats.Bmp
{
using static TestImages.Bmp;
@@ -262,4 +264,4 @@ private static void TestBmpEncoderCore(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetaDataTests.cs
index ab72214f6c..da17dfb983 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetaDataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetaDataTests.cs
@@ -1,11 +1,17 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
+using System.IO;
+
using SixLabors.ImageSharp.Formats.Bmp;
using Xunit;
+// ReSharper disable InconsistentNaming
+
namespace SixLabors.ImageSharp.Tests.Formats.Bmp
{
+ using static TestImages.Bmp;
+
public class BmpMetaDataTests
{
[Fact]
@@ -18,5 +24,27 @@ public void CloneIsDeep()
Assert.False(meta.BitsPerPixel.Equals(clone.BitsPerPixel));
}
+
+ [Theory]
+ [InlineData(WinBmpv2, BmpInfoHeaderType.WinVersion2)]
+ [InlineData(WinBmpv3, BmpInfoHeaderType.WinVersion3)]
+ [InlineData(WinBmpv4, BmpInfoHeaderType.WinVersion4)]
+ [InlineData(WinBmpv5, BmpInfoHeaderType.WinVersion5)]
+ [InlineData(Os2v2Short, BmpInfoHeaderType.Os2Version2Short)]
+ [InlineData(Rgb32h52AdobeV3, BmpInfoHeaderType.AdobeVersion3)]
+ [InlineData(Rgba32bf56AdobeV3, BmpInfoHeaderType.AdobeVersion3WithAlpha)]
+ [InlineData(Os2v2, BmpInfoHeaderType.Os2Version2)]
+ public void Identify_DetectsCorrectBitmapInfoHeaderType(string imagePath, BmpInfoHeaderType expectedInfoHeaderType)
+ {
+ var testFile = TestFile.Create(imagePath);
+ using (var stream = new MemoryStream(testFile.Bytes, false))
+ {
+ IImageInfo imageInfo = Image.Identify(stream);
+ Assert.NotNull(imageInfo);
+ BmpMetadata bitmapMetaData = imageInfo.Metadata.GetFormatMetadata(BmpFormat.Instance);
+ Assert.NotNull(bitmapMetaData);
+ Assert.Equal(expectedInfoHeaderType, bitmapMetaData.InfoHeaderType);
+ }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index d041f48544..1019a5b084 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -261,6 +261,14 @@ public static class Bmp
public const string Bit8Palette4 = "Bmp/pal8-0.bmp";
public const string Os2v2Short = "Bmp/pal8os2v2-16.bmp";
public const string Os2v2 = "Bmp/pal8os2v2.bmp";
+ public const string Os2BitmapArray9s = "Bmp/9S.BMP";
+ public const string Os2BitmapArrayDiamond = "Bmp/DIAMOND.BMP";
+ public const string Os2BitmapArrayMarble = "Bmp/GMARBLE.BMP";
+ public const string Os2BitmapArraySkater = "Bmp/SKATER.BMP";
+ public const string Os2BitmapArraySpade = "Bmp/SPADE.BMP";
+ public const string Os2BitmapArraySunflower = "Bmp/SUNFLOW.BMP";
+ public const string Os2BitmapArrayWarpd = "Bmp/WARPD.BMP";
+ public const string Os2BitmapArrayPines = "Bmp/PINES.BMP";
public const string LessThanFullSizedPalette = "Bmp/pal8os2sp.bmp";
public const string Pal8Offset = "Bmp/pal8offs.bmp";
public const string OversizedPalette = "Bmp/pal8oversizepal.bmp";
diff --git a/tests/Images/Input/Bmp/9S.BMP b/tests/Images/Input/Bmp/9S.BMP
new file mode 100644
index 0000000000..c889ec75eb
Binary files /dev/null and b/tests/Images/Input/Bmp/9S.BMP differ
diff --git a/tests/Images/Input/Bmp/DIAMOND.BMP b/tests/Images/Input/Bmp/DIAMOND.BMP
new file mode 100644
index 0000000000..fff96d00e6
Binary files /dev/null and b/tests/Images/Input/Bmp/DIAMOND.BMP differ
diff --git a/tests/Images/Input/Bmp/GMARBLE.BMP b/tests/Images/Input/Bmp/GMARBLE.BMP
new file mode 100644
index 0000000000..6d865a9e7e
Binary files /dev/null and b/tests/Images/Input/Bmp/GMARBLE.BMP differ
diff --git a/tests/Images/Input/Bmp/PINES.BMP b/tests/Images/Input/Bmp/PINES.BMP
new file mode 100644
index 0000000000..63dcc9f0fa
Binary files /dev/null and b/tests/Images/Input/Bmp/PINES.BMP differ
diff --git a/tests/Images/Input/Bmp/SKATER.BMP b/tests/Images/Input/Bmp/SKATER.BMP
new file mode 100644
index 0000000000..ad0e24d283
Binary files /dev/null and b/tests/Images/Input/Bmp/SKATER.BMP differ
diff --git a/tests/Images/Input/Bmp/SPADE.BMP b/tests/Images/Input/Bmp/SPADE.BMP
new file mode 100644
index 0000000000..31c0c02e78
Binary files /dev/null and b/tests/Images/Input/Bmp/SPADE.BMP differ
diff --git a/tests/Images/Input/Bmp/SUNFLOW.BMP b/tests/Images/Input/Bmp/SUNFLOW.BMP
new file mode 100644
index 0000000000..852355224f
Binary files /dev/null and b/tests/Images/Input/Bmp/SUNFLOW.BMP differ
diff --git a/tests/Images/Input/Bmp/WARPD.BMP b/tests/Images/Input/Bmp/WARPD.BMP
new file mode 100644
index 0000000000..ecfcd79b00
Binary files /dev/null and b/tests/Images/Input/Bmp/WARPD.BMP differ