From 2c621120175e5503f51aabe544f0ec79980302fc Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Wed, 23 Jan 2019 13:22:58 +0100 Subject: [PATCH] Adds support for OS/2 version 2 bitmaps (#813) * Added support for OS/2 version 2 bitmaps * throw NotSupportedException, if the file header type is not BM * renamed Os2v2 to Os2v2Size * Added BmpThrowHelper similar to the JpegThrowHelper --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 41 +++++++++----- src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 54 ++++++++++++++++++- src/ImageSharp/Formats/Bmp/BmpThrowHelper.cs | 28 ++++++++++ .../Formats/Bmp/BmpDecoderTests.cs | 18 ++++++- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Bmp/pal8os2v2.bmp | 3 ++ 6 files changed, 129 insertions(+), 16 deletions(-) create mode 100644 src/ImageSharp/Formats/Bmp/BmpThrowHelper.cs create mode 100644 tests/Images/Input/Bmp/pal8os2v2.bmp diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 8ca698b878..6bfdfa3a29 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 316df4acc9..6da5f73e3f 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 0000000000..dae044ddbd --- /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 0ebfbf311a..5d7d35dd58 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 6e6c7ce47a..d83fe4907d 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 0000000000..11473a632b --- /dev/null +++ b/tests/Images/Input/Bmp/pal8os2v2.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2597b53091d734900068b42cddaed7430261cc5eade32b4f19fd6c2c4568a52 +size 9278