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