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
zcmbuDUrbb29>*_#1ly_)t|lZlX5;oncwxBcj5dYM8k(5U^g;BY^56_`88DGP5Sp0C
z(xirmj)}~maN$Ar!9a{DiHU{>Fs;Li4);MwOzVRwiHXJsmfa2bk4^4=e&?KF{xH~X
z_M9H>aE3d-&*%3&7fJu}McpTs8g-?X^=DrFl_l3AYdc>Vf3&PWe5BWZ)W&apvH1aq
z<+41MZ&_7bs@Sh$zl!}T_N&-G$^J?9PqKfK{gdpUWWSI7KKA?A?_d;{|5Uv*niCaWA-1j|Cs&9>_29o|16(A>m0>A>m0>A>m0>A>m0>A>m0>A>m0>A>m0>A>m0>A>m0>A>m0NoQHoNuiYK
zvDk;xh0}%8h0}%8h0}%8h0}%8h0}%8h0}%8h0}%8?PDKK7fu&W7fu&W7fu&W7fu&W
z7fu&W7fu&W7fu&W7fu&W7fu&W7f#Y=N!rp$p_Iu!oF1GWoF1GWoF1GWoF1GWoF1GW
zoF1GWoF1GWoF1HBANz26aC&fhaC&fhaC&fhaC&fhaC&fhaC&fhaC&fhaC&fhaC&gE
z&5{!-X-g-Ck_o2|rw^wOrw^wOrw^wOrw^wOrw^wOrw^wOrw^wOrw^y!$3C1soIadB
zoIadBoIadBoIadBoIadBoFK8-htr4Chtro$%PO;~8HW#_Ipa8&oy(W6GVcF)pYiI|
zEBOJdN{v&DZ%=)z#!WST{`qHhe>r34&Rx6q?Ag0#@7{fk10Nq?96We1^8Uf?)mC-c
z;p#KhXAV1ODDPao{NvU80Q~d*D*z}iv+9&gDG$I+nKyryN&RxGyliLruJT_DuZM{rHFQ
zuisO@rvd-QeT^S~f&U5dHxv5^+2>CAM^fh>*7;u^F!@UW;;#bW{1d<_0{FI{{0ZQu
z3V`!h$^GIV()sVLH~C8d;vXgJ>=oyGy<%J`U!LXPPX6Lg2ky@qf7JowuR37-RR{2|
zmivu=9r=qt9XQaa{1eJL8qwD{SKR?~6@LJ#1J%L9;WO>Df8g@a)gjui0A9Ve%E|x;
z1gZjo=2Okhw7ZS+mT17=xQ91sB@ZoT{{Y-m1{zC*XhyUE`SFh18E5kp~9B5Vp
z|NdM3*fa32th6et@ehP{hC+3_>gw=sAb>{v8&BY$HkNB{_OVzxe*o0^2SDXN#QB%}
z0g&=R04N`b0MOsu-#=vlo&liR4}d!V0I2*MIRBDA0Ft|2I(x}IYvEs1UL=0OApVSj
zfg#2m4fxmG>(?275H|ejfY@vQ%IZpeKmLsR`UXZL4fvw*#0l}wojFm&s2?bj_JsrC
zV0(C=eV~14;HRNE04NW@4zVu_1|rPn=1VfMpX#5wh5fB(^gmE3@r44RU|p!buD-6J
z{_}=L04NW@mM3373F1%wjIj8J(Z~O%A@mv9{w9BkKR5qM@@IsC_=nKP|MLd)8Tdzy
zeN_MYiE>pl&R5C5nD*BI5CI?xz%c&z0C+(8H_Bf+p#3Aw5#^6P{?dVGPn&4}X#gSs
zL;>i+|7!qxD1QNf_hc^}$j^TuD}U|3qYQw6%yjun{g+gE0G>XpL_aHk0@%o3`EQ+m
z4F4GZHTZ|%kKi9~kGBuw|6u6BPyfdMjmkgcuk+Xb*en0j_=olVp^mzax-R^C8hSqe
z2LB8AzjssQ@^C`0v0z2w;Q`M6kaEz!U*YiGLFfIIZpv
zg$SUo1AhW&Q~~JxC3~~tlD}lHm-uhTzsC4S$=~?Pzs&i=CjXcCzr_D^)9J7D
zThWNgzpDp;Z+b2O@SdkXX8g&Y5fOjs0RH!eb^ez1X2YteC~tZjN&DYb{=KyS
zqVeaL>AUbBqy6LJFS+M0$zHGg{I!23fBbg?5M-MEClU!=;rRZ;wKLL=xGS75i
z!(4X&e;a=r{}%iqjk@?hO#A1Zf7-v{ukyF7ri)qr1}*0zQ)?OLzcm
z@^2x30$_W1IJuF3_9sdDQS{UPckjNGw`ssd-4Xs}*Dtc~lGhu#%NfY&0RA6j`FD`N
z_|pOTq`X0S>5w`9~1+{cdyrlyiLX*98K>Puyv1^WmS|T0tBXM`w9e${u
z|IPJzKjW{9Ndy{%$G1>@1t3KWOq2hS-;=-Yf7CVoU(rQncjNpQ${z{IEiIw1Z4*tPc2Bdsl+9i8}d
zdfgPy|K@Ryq5NHCZ~OtcIC+r>V$+&OZhsi(fpOt@d_2IjR@80gOI4<*8w@mDr
zaysx|lWFqD|KgqR$v-|5PwMkCGC#I3wlI#h%tG0#{7XwqN
z?rbXcFqL}u_FcvPiVDgfqx`vZ-RNJV0V(`d0Q7&Ncj5>9C+~cJ=MDkP%p{UYb^ll@
zwJ^SraK)Yc3teZn%AY6TVaoWc4j6yc0pqVakjY>9PscdZ_;deQO8Y02J-;<{50YF}
z{t12lOG`^XEUjf6J&J$lAPpE{q-X&C@5CQ~V=}w(m-fr#edgNA+Qh`f4-=D&+qdzb
zo}mFrhQ5DcVZkJ??0KDkmHbP2{%d*ukMjI?^861%KaxtWrpW)@@5q0G=YNv?Z{AZK;e(@LMLMzFBGGT3_{f7W(1K=zGqn#sze;*P0RB9ans#hG}PxFs+{<{gF
zo918R{HJJtYHeka_CEn&34rGSEKetA{*jdK$^H0C{*t}qzOm$#sAc8lul;rY1d!!V
z2R8ia0E6<%KRbWzuk$AWl`~jGJ
z!t=j$=Q-`4nOROQ)BcnIEUX)EZJ+sSf=K88>6ZD6J=6GOulyh3k6miroD~)~D75{A?Z9ByH^i%v9SFVgQ=4rs%Jb%f2BI8Rn&Z}`TVwtf@1J+h~KhIvek+Dy@^89Zz&;O_9`M+YG|9N@-e@Fg{=K25CJpV7u
z^S`Rk|CZTHH^iU(8ExX4NR=gTiB
SzpTmwAld81-vt=wE$hEMUm(B$
literal 0
HcmV?d00001