From b03d41bbb92e57bc2eab3285decd8f4d07856963 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 | Bin 0 -> 9278 bytes 6 files changed, 126 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 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 0000000000000000000000000000000000000000..1324a40d00a417649e67f2c4eb8f23f9f7ee1c3d GIT binary patch literal 9278 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 SzpTmwAld81-vt=wE$hEMUm(B$ literal 0 HcmV?d00001