Skip to content

Commit b03d41b

Browse files
brianpopowJimBobSquarePants
authored andcommitted
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
1 parent 1b9979c commit b03d41b

6 files changed

Lines changed: 126 additions & 16 deletions

File tree

src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ public Image<TPixel> Decode<TPixel>(Stream stream)
168168
break;
169169

170170
default:
171-
throw new NotSupportedException("Does not support this kind of bitmap files.");
171+
BmpThrowHelper.ThrowNotSupportedException("Does not support this kind of bitmap files.");
172+
173+
break;
172174
}
173175

174176
return image;
@@ -319,7 +321,7 @@ private void UncompressRle4(int w, Span<byte> buffer)
319321
{
320322
if (this.stream.Read(cmd, 0, cmd.Length) != 2)
321323
{
322-
throw new Exception("Failed to read 2 bytes from the stream");
324+
BmpThrowHelper.ThrowImageFormatException("Failed to read 2 bytes from the stream while uncompressing RLE4 bitmap.");
323325
}
324326

325327
if (cmd[0] == RleCommand)
@@ -429,7 +431,7 @@ private void UncompressRle8(int w, Span<byte> buffer)
429431
{
430432
if (this.stream.Read(cmd, 0, cmd.Length) != 2)
431433
{
432-
throw new Exception("Failed to read 2 bytes from stream");
434+
BmpThrowHelper.ThrowImageFormatException("Failed to read 2 bytes from stream while uncompressing RLE8 bitmap.");
433435
}
434436

435437
if (cmd[0] == RleCommand)
@@ -913,7 +915,7 @@ private void ReadInfoHeader()
913915
int headerSize = BinaryPrimitives.ReadInt32LittleEndian(buffer);
914916
if (headerSize < BmpInfoHeader.CoreSize)
915917
{
916-
throw new NotSupportedException($"ImageSharp does not support this BMP file. HeaderSize: {headerSize}.");
918+
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize is '{headerSize}'.");
917919
}
918920

919921
int skipAmount = 0;
@@ -926,23 +928,23 @@ private void ReadInfoHeader()
926928
// read the rest of the header
927929
this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize);
928930

929-
BmpInfoHeaderType inofHeaderType = BmpInfoHeaderType.WinVersion2;
931+
BmpInfoHeaderType infoHeaderType = BmpInfoHeaderType.WinVersion2;
930932
if (headerSize == BmpInfoHeader.CoreSize)
931933
{
932934
// 12 bytes
933-
inofHeaderType = BmpInfoHeaderType.WinVersion2;
935+
infoHeaderType = BmpInfoHeaderType.WinVersion2;
934936
this.infoHeader = BmpInfoHeader.ParseCore(buffer);
935937
}
936938
else if (headerSize == BmpInfoHeader.Os22ShortSize)
937939
{
938940
// 16 bytes
939-
inofHeaderType = BmpInfoHeaderType.Os2Version2Short;
941+
infoHeaderType = BmpInfoHeaderType.Os2Version2Short;
940942
this.infoHeader = BmpInfoHeader.ParseOs22Short(buffer);
941943
}
942944
else if (headerSize == BmpInfoHeader.SizeV3)
943945
{
944946
// == 40 bytes
945-
inofHeaderType = BmpInfoHeaderType.WinVersion3;
947+
infoHeaderType = BmpInfoHeaderType.WinVersion3;
946948
this.infoHeader = BmpInfoHeader.ParseV3(buffer);
947949

948950
// if the info header is BMP version 3 and the compression type is BITFIELDS,
@@ -960,24 +962,30 @@ private void ReadInfoHeader()
960962
else if (headerSize == BmpInfoHeader.AdobeV3Size)
961963
{
962964
// == 52 bytes
963-
inofHeaderType = BmpInfoHeaderType.AdobeVersion3;
965+
infoHeaderType = BmpInfoHeaderType.AdobeVersion3;
964966
this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: false);
965967
}
966968
else if (headerSize == BmpInfoHeader.AdobeV3WithAlphaSize)
967969
{
968970
// == 56 bytes
969-
inofHeaderType = BmpInfoHeaderType.AdobeVersion3WithAlpha;
971+
infoHeaderType = BmpInfoHeaderType.AdobeVersion3WithAlpha;
970972
this.infoHeader = BmpInfoHeader.ParseAdobeV3(buffer, withAlpha: true);
971973
}
974+
else if (headerSize == BmpInfoHeader.Os2v2Size)
975+
{
976+
// == 64 bytes
977+
infoHeaderType = BmpInfoHeaderType.Os2Version2;
978+
this.infoHeader = BmpInfoHeader.ParseOs2Version2(buffer);
979+
}
972980
else if (headerSize >= BmpInfoHeader.SizeV4)
973981
{
974982
// >= 108 bytes
975-
inofHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5;
983+
infoHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5;
976984
this.infoHeader = BmpInfoHeader.ParseV4(buffer);
977985
}
978986
else
979987
{
980-
throw new NotSupportedException($"ImageSharp does not support this BMP file. HeaderSize: {headerSize}.");
988+
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'.");
981989
}
982990

983991
// Resolution is stored in PPM.
@@ -1001,7 +1009,7 @@ private void ReadInfoHeader()
10011009

10021010
short bitsPerPixel = this.infoHeader.BitsPerPixel;
10031011
this.bmpMetaData = this.metaData.GetFormatMetaData(BmpFormat.Instance);
1004-
this.bmpMetaData.InfoHeaderType = inofHeaderType;
1012+
this.bmpMetaData.InfoHeaderType = infoHeaderType;
10051013

10061014
// We can only encode at these bit rates so far.
10071015
if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24)
@@ -1027,6 +1035,11 @@ private void ReadFileHeader()
10271035
this.stream.Read(buffer, 0, BmpFileHeader.Size);
10281036

10291037
this.fileHeader = BmpFileHeader.Parse(buffer);
1038+
1039+
if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
1040+
{
1041+
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{this.fileHeader.Type}'.");
1042+
}
10301043
}
10311044

10321045
/// <summary>
@@ -1080,7 +1093,7 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett
10801093
// 256 * 4
10811094
if (colorMapSize > 1024)
10821095
{
1083-
throw new ImageFormatException($"Invalid bmp colormap size '{colorMapSize}'");
1096+
BmpThrowHelper.ThrowImageFormatException($"Invalid bmp colormap size '{colorMapSize}'");
10841097
}
10851098

10861099
palette = new byte[colorMapSize];

src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ internal struct BmpInfoHeader
4141
/// </summary>
4242
public const int AdobeV3WithAlphaSize = 56;
4343

44+
/// <summary>
45+
/// Size of a IBM OS/2 2.x bitmap header.
46+
/// </summary>
47+
public const int Os2v2Size = 64;
48+
4449
/// <summary>
4550
/// Defines the size of the BITMAPINFOHEADER (BMP Version 4) data structure in the bitmap file.
4651
/// </summary>
@@ -117,7 +122,7 @@ public BmpInfoHeader(
117122
}
118123

119124
/// <summary>
120-
/// Gets or sets the size of this header
125+
/// Gets or sets the size of this header.
121126
/// </summary>
122127
public int HeaderSize { get; set; }
123128

@@ -346,6 +351,53 @@ public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan<byte> data, bool withAlpha
346351
alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0);
347352
}
348353

354+
/// <summary>
355+
/// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are
356+
/// very similar to the Bitmap v3 header. The other 24 bytes are ignored, but they do not hold any
357+
/// useful information for decoding the image.
358+
/// </summary>
359+
/// <param name="data">The data to parse.</param>
360+
/// <returns>The parsed header.</returns>
361+
/// <seealso href="https://www.fileformat.info/format/os2bmp/egff.htm"/>
362+
public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan<byte> data)
363+
{
364+
var infoHeader = new BmpInfoHeader(
365+
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
366+
width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
367+
height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
368+
planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)),
369+
bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)));
370+
371+
int compression = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4));
372+
373+
// The compression value in OS/2 bitmap has a different meaning than in windows bitmaps.
374+
// Map the OS/2 value to the windows values.
375+
switch (compression)
376+
{
377+
case 0:
378+
infoHeader.Compression = BmpCompression.RGB;
379+
break;
380+
case 1:
381+
infoHeader.Compression = BmpCompression.RLE8;
382+
break;
383+
case 2:
384+
infoHeader.Compression = BmpCompression.RLE4;
385+
break;
386+
default:
387+
BmpThrowHelper.ThrowImageFormatException($"Compression type is not supported. ImageSharp only supports uncompressed, RLE4 and RLE8.");
388+
break;
389+
}
390+
391+
infoHeader.ImageSize = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4));
392+
infoHeader.XPelsPerMeter = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4));
393+
infoHeader.YPelsPerMeter = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4));
394+
infoHeader.ClrUsed = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4));
395+
infoHeader.ClrImportant = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4));
396+
397+
// The following 24 bytes of the header are omitted.
398+
return infoHeader;
399+
}
400+
349401
/// <summary>
350402
/// Parses the full BMP Version 4 BITMAPINFOHEADER header (108 bytes).
351403
/// </summary>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace SixLabors.ImageSharp.Formats.Bmp
5+
{
6+
internal static class BmpThrowHelper
7+
{
8+
/// <summary>
9+
/// Cold path optimization for throwing <see cref="ImageFormatException"/>-s
10+
/// </summary>
11+
/// <param name="errorMessage">The error message for the exception.</param>
12+
[MethodImpl(MethodImplOptions.NoInlining)]
13+
public static void ThrowImageFormatException(string errorMessage)
14+
{
15+
throw new ImageFormatException(errorMessage);
16+
}
17+
18+
/// <summary>
19+
/// Cold path optimization for throwing <see cref="NotSupportedException"/>-s
20+
/// </summary>
21+
/// <param name="errorMessage">The error message for the exception.</param>
22+
[MethodImpl(MethodImplOptions.NoInlining)]
23+
public static void ThrowNotSupportedException(string errorMessage)
24+
{
25+
throw new NotSupportedException(errorMessage);
26+
}
27+
}
28+
}

tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,28 @@ public void BmpDecoder_CanDecode_Os2v2XShortHeader<TPixel>(TestImageProvider<TPi
214214
{
215215
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
216216
{
217-
image.DebugSave(provider, "png");
217+
image.DebugSave(provider);
218218

219219
// TODO: Neither System.Drawing not MagickReferenceDecoder
220220
// can correctly decode this file.
221221
// image.CompareToOriginal(provider);
222222
}
223223
}
224+
225+
[Theory]
226+
[WithFile(Os2v2, PixelTypes.Rgba32)]
227+
public void BmpDecoder_CanDecode_Os2v2Header<TPixel>(TestImageProvider<TPixel> provider)
228+
where TPixel : struct, IPixel<TPixel>
229+
{
230+
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
231+
{
232+
image.DebugSave(provider);
233+
234+
// TODO: System.Drawing can not decode this image. MagickReferenceDecoder can decode it,
235+
// but i think incorrectly. I have loaded the image with GIMP and exported as PNG.
236+
// The results are the same as the image sharp implementation.
237+
// image.CompareToOriginal(provider, new MagickReferenceDecoder());
238+
}
239+
}
224240
}
225241
}

tests/ImageSharp.Tests/TestImages.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ public static class Bmp
216216
public const string WinBmpv5 = "Bmp/pal8v5.bmp";
217217
public const string Bit8Palette4 = "Bmp/pal8-0.bmp";
218218
public const string Os2v2Short = "Bmp/pal8os2v2-16.bmp";
219+
public const string Os2v2 = "Bmp/pal8os2v2.bmp";
219220

220221
// Bitmap images with compression type BITFIELDS
221222
public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp";
9.06 KB
Binary file not shown.

0 commit comments

Comments
 (0)