diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 8ca698b87..6bfdfa3a2 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -168,7 +168,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp break; default: - throw new NotSupportedException("Does not support this kind of bitmap files."); + BmpThrowHelper.ThrowNotSupportedException("Does not support this kind of bitmap files."); + + break; } return image; @@ -319,7 +321,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp { if (this.stream.Read(cmd, 0, cmd.Length) != 2) { - throw new Exception("Failed to read 2 bytes from the stream"); + BmpThrowHelper.ThrowImageFormatException("Failed to read 2 bytes from the stream while uncompressing RLE4 bitmap."); } if (cmd[0] == RleCommand) @@ -429,7 +431,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp { if (this.stream.Read(cmd, 0, cmd.Length) != 2) { - throw new Exception("Failed to read 2 bytes from stream"); + BmpThrowHelper.ThrowImageFormatException("Failed to read 2 bytes from stream while uncompressing RLE8 bitmap."); } if (cmd[0] == RleCommand) @@ -913,7 +915,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp int headerSize = BinaryPrimitives.ReadInt32LittleEndian(buffer); if (headerSize < BmpInfoHeader.CoreSize) { - throw new NotSupportedException($"ImageSharp does not support this BMP file. HeaderSize: {headerSize}."); + BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize is '{headerSize}'."); } int skipAmount = 0; @@ -926,23 +928,23 @@ namespace SixLabors.ImageSharp.Formats.Bmp // read the rest of the header this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize); - BmpInfoHeaderType inofHeaderType = BmpInfoHeaderType.WinVersion2; + BmpInfoHeaderType infoHeaderType = BmpInfoHeaderType.WinVersion2; if (headerSize == BmpInfoHeader.CoreSize) { // 12 bytes - inofHeaderType = BmpInfoHeaderType.WinVersion2; + infoHeaderType = BmpInfoHeaderType.WinVersion2; this.infoHeader = BmpInfoHeader.ParseCore(buffer); } else if (headerSize == BmpInfoHeader.Os22ShortSize) { // 16 bytes - inofHeaderType = BmpInfoHeaderType.Os2Version2Short; + infoHeaderType = BmpInfoHeaderType.Os2Version2Short; this.infoHeader = BmpInfoHeader.ParseOs22Short(buffer); } else if (headerSize == BmpInfoHeader.SizeV3) { // == 40 bytes - inofHeaderType = BmpInfoHeaderType.WinVersion3; + infoHeaderType = BmpInfoHeaderType.WinVersion3; this.infoHeader = BmpInfoHeader.ParseV3(buffer); // if the info header is BMP version 3 and the compression type is BITFIELDS, @@ -960,24 +962,30 @@ namespace SixLabors.ImageSharp.Formats.Bmp else if (headerSize == BmpInfoHeader.AdobeV3Size) { // == 52 bytes - inofHeaderType = BmpInfoHeaderType.AdobeVersion3; + infoHeaderType = BmpInfoHeaderType.AdobeVersion3; this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: false); } else if (headerSize == BmpInfoHeader.AdobeV3WithAlphaSize) { // == 56 bytes - inofHeaderType = BmpInfoHeaderType.AdobeVersion3WithAlpha; + infoHeaderType = BmpInfoHeaderType.AdobeVersion3WithAlpha; this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: true); } + else if (headerSize == BmpInfoHeader.Os2v2Size) + { + // == 64 bytes + infoHeaderType = BmpInfoHeaderType.Os2Version2; + this.infoHeader = BmpInfoHeader.ParseOs2Version2(buffer); + } else if (headerSize >= BmpInfoHeader.SizeV4) { // >= 108 bytes - inofHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5; + infoHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5; this.infoHeader = BmpInfoHeader.ParseV4(buffer); } else { - throw new NotSupportedException($"ImageSharp does not support this BMP file. HeaderSize: {headerSize}."); + BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'."); } // Resolution is stored in PPM. @@ -1001,7 +1009,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp short bitsPerPixel = this.infoHeader.BitsPerPixel; this.bmpMetaData = this.metaData.GetFormatMetaData(BmpFormat.Instance); - this.bmpMetaData.InfoHeaderType = inofHeaderType; + this.bmpMetaData.InfoHeaderType = infoHeaderType; // We can only encode at these bit rates so far. if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24) @@ -1027,6 +1035,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.stream.Read(buffer, 0, BmpFileHeader.Size); this.fileHeader = BmpFileHeader.Parse(buffer); + + if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap) + { + BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{this.fileHeader.Type}'."); + } } /// @@ -1080,7 +1093,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp // 256 * 4 if (colorMapSize > 1024) { - throw new ImageFormatException($"Invalid bmp colormap size '{colorMapSize}'"); + BmpThrowHelper.ThrowImageFormatException($"Invalid bmp colormap size '{colorMapSize}'"); } palette = new byte[colorMapSize]; diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 316df4acc..6da5f73e3 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -41,6 +41,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public const int AdobeV3WithAlphaSize = 56; + /// + /// Size of a IBM OS/2 2.x bitmap header. + /// + public const int Os2v2Size = 64; + /// /// Defines the size of the BITMAPINFOHEADER (BMP Version 4) data structure in the bitmap file. /// @@ -117,7 +122,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp } /// - /// Gets or sets the size of this header + /// Gets or sets the size of this header. /// public int HeaderSize { get; set; } @@ -346,6 +351,53 @@ namespace SixLabors.ImageSharp.Formats.Bmp alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0); } + /// + /// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are + /// very similar to the Bitmap v3 header. The other 24 bytes are ignored, but they do not hold any + /// useful information for decoding the image. + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan data) + { + var infoHeader = 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))); + + int compression = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)); + + // The compression value in OS/2 bitmap has a different meaning than in windows bitmaps. + // Map the OS/2 value to the windows values. + switch (compression) + { + case 0: + infoHeader.Compression = BmpCompression.RGB; + break; + case 1: + infoHeader.Compression = BmpCompression.RLE8; + break; + case 2: + infoHeader.Compression = BmpCompression.RLE4; + break; + default: + BmpThrowHelper.ThrowImageFormatException($"Compression type is not supported. ImageSharp only supports uncompressed, RLE4 and RLE8."); + break; + } + + infoHeader.ImageSize = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)); + infoHeader.XPelsPerMeter = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)); + infoHeader.YPelsPerMeter = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)); + infoHeader.ClrUsed = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)); + infoHeader.ClrImportant = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)); + + // The following 24 bytes of the header are omitted. + return infoHeader; + } + /// /// Parses the full BMP Version 4 BITMAPINFOHEADER header (108 bytes). /// diff --git a/src/ImageSharp/Formats/Bmp/BmpThrowHelper.cs b/src/ImageSharp/Formats/Bmp/BmpThrowHelper.cs new file mode 100644 index 000000000..dae044ddb --- /dev/null +++ b/src/ImageSharp/Formats/Bmp/BmpThrowHelper.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Bmp +{ + internal static class BmpThrowHelper + { + /// + /// Cold path optimization for throwing -s + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowImageFormatException(string errorMessage) + { + throw new ImageFormatException(errorMessage); + } + + /// + /// Cold path optimization for throwing -s + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowNotSupportedException(string errorMessage) + { + throw new NotSupportedException(errorMessage); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 0ebfbf311..5d7d35dd5 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -214,12 +214,28 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = provider.GetImage(new BmpDecoder())) { - image.DebugSave(provider, "png"); + image.DebugSave(provider); // TODO: Neither System.Drawing not MagickReferenceDecoder // can correctly decode this file. // image.CompareToOriginal(provider); } } + + [Theory] + [WithFile(Os2v2, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecode_Os2v2Header(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + image.DebugSave(provider); + + // TODO: System.Drawing can not decode this image. MagickReferenceDecoder can decode it, + // but i think incorrectly. I have loaded the image with GIMP and exported as PNG. + // The results are the same as the image sharp implementation. + // image.CompareToOriginal(provider, new MagickReferenceDecoder()); + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 6e6c7ce47..d83fe4907 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -216,6 +216,7 @@ namespace SixLabors.ImageSharp.Tests public const string WinBmpv5 = "Bmp/pal8v5.bmp"; public const string Bit8Palette4 = "Bmp/pal8-0.bmp"; public const string Os2v2Short = "Bmp/pal8os2v2-16.bmp"; + public const string Os2v2 = "Bmp/pal8os2v2.bmp"; // Bitmap images with compression type BITFIELDS public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp"; diff --git a/tests/Images/Input/Bmp/pal8os2v2.bmp b/tests/Images/Input/Bmp/pal8os2v2.bmp new file mode 100644 index 000000000..1324a40d0 Binary files /dev/null and b/tests/Images/Input/Bmp/pal8os2v2.bmp differ