Skip to content

Commit 6016c11

Browse files
brianpopowJimBobSquarePants
authored andcommitted
Bitmap decoder now can decode bitmap arrays (#930)
* Bitmap Decoder can now decode BitmapArray * Add tests for bitmap metadata decoing. Fix an issue that a bitmap with a v5 header would be set in the metadata as an v4 header. * Fixed issue with decoding bitmap arrays: color map size was not determined correctly. Added more test images. * Refactor colormap size duplicate declaration. * Fixed an issue, that when an unsupported bitmap is loaded the typ marker was not correctly shown in the error message
1 parent 7a484f7 commit 6016c11

File tree

17 files changed

+216
-47
lines changed

17 files changed

+216
-47
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) Six Labors and contributors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Runtime.InteropServices;
6+
7+
namespace SixLabors.ImageSharp.Formats.Bmp
8+
{
9+
[StructLayout(LayoutKind.Sequential, Pack = 1)]
10+
internal readonly struct BmpArrayFileHeader
11+
{
12+
public BmpArrayFileHeader(short type, int size, int offsetToNext, short width, short height)
13+
{
14+
this.Type = type;
15+
this.Size = size;
16+
this.OffsetToNext = offsetToNext;
17+
this.ScreenWidth = width;
18+
this.ScreenHeight = height;
19+
}
20+
21+
/// <summary>
22+
/// Gets the Bitmap identifier.
23+
/// The field used to identify the bitmap file: 0x42 0x41 (Hex code points for B and A).
24+
/// </summary>
25+
public short Type { get; }
26+
27+
/// <summary>
28+
/// Gets the size of this header.
29+
/// </summary>
30+
public int Size { get; }
31+
32+
/// <summary>
33+
/// Gets the offset to next OS2BMPARRAYFILEHEADER.
34+
/// This offset is calculated from the starting byte of the file. A value of zero indicates that this header is for the last image in the array list.
35+
/// </summary>
36+
public int OffsetToNext { get; }
37+
38+
/// <summary>
39+
/// Gets the width of the image display in pixels.
40+
/// </summary>
41+
public short ScreenWidth { get; }
42+
43+
/// <summary>
44+
/// Gets the height of the image display in pixels.
45+
/// </summary>
46+
public short ScreenHeight { get; }
47+
48+
public static BmpArrayFileHeader Parse(Span<byte> data)
49+
{
50+
return MemoryMarshal.Cast<byte, BmpArrayFileHeader>(data)[0];
51+
}
52+
}
53+
}

src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,18 +1053,11 @@ private void ReadInfoHeader()
10531053
this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize);
10541054

10551055
int headerSize = BinaryPrimitives.ReadInt32LittleEndian(buffer);
1056-
if (headerSize < BmpInfoHeader.CoreSize)
1056+
if (headerSize < BmpInfoHeader.CoreSize || headerSize > BmpInfoHeader.MaxHeaderSize)
10571057
{
10581058
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize is '{headerSize}'.");
10591059
}
10601060

1061-
int skipAmount = 0;
1062-
if (headerSize > BmpInfoHeader.MaxHeaderSize)
1063-
{
1064-
skipAmount = headerSize - BmpInfoHeader.MaxHeaderSize;
1065-
headerSize = BmpInfoHeader.MaxHeaderSize;
1066-
}
1067-
10681061
// Read the rest of the header.
10691062
this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize);
10701063

@@ -1169,15 +1162,13 @@ private void ReadInfoHeader()
11691162
{
11701163
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
11711164
}
1172-
1173-
// Skip the remaining header because we can't read those parts.
1174-
this.stream.Skip(skipAmount);
11751165
}
11761166

11771167
/// <summary>
11781168
/// Reads the <see cref="BmpFileHeader"/> from the stream.
11791169
/// </summary>
1180-
private void ReadFileHeader()
1170+
/// <returns>The color map size in bytes, if it could be determined by the file header. Otherwise -1.</returns>
1171+
private int ReadFileHeader()
11811172
{
11821173
#if NETCOREAPP2_1
11831174
Span<byte> buffer = stackalloc byte[BmpFileHeader.Size];
@@ -1186,12 +1177,36 @@ private void ReadFileHeader()
11861177
#endif
11871178
this.stream.Read(buffer, 0, BmpFileHeader.Size);
11881179

1189-
this.fileHeader = BmpFileHeader.Parse(buffer);
1190-
1191-
if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
1180+
short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(buffer);
1181+
switch (fileTypeMarker)
11921182
{
1193-
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{this.fileHeader.Type}'.");
1183+
case BmpConstants.TypeMarkers.Bitmap:
1184+
this.fileHeader = BmpFileHeader.Parse(buffer);
1185+
break;
1186+
case BmpConstants.TypeMarkers.BitmapArray:
1187+
// The Array file header is followed by the bitmap file header of the first image.
1188+
var arrayHeader = BmpArrayFileHeader.Parse(buffer);
1189+
this.stream.Read(buffer, 0, BmpFileHeader.Size);
1190+
this.fileHeader = BmpFileHeader.Parse(buffer);
1191+
if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
1192+
{
1193+
BmpThrowHelper.ThrowNotSupportedException($"Unsupported bitmap file inside a BitmapArray file. File header bitmap type marker '{this.fileHeader.Type}'.");
1194+
}
1195+
1196+
if (arrayHeader.OffsetToNext != 0)
1197+
{
1198+
int colorMapSizeBytes = arrayHeader.OffsetToNext - arrayHeader.Size;
1199+
return colorMapSizeBytes;
1200+
}
1201+
1202+
break;
1203+
1204+
default:
1205+
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{fileTypeMarker}'.");
1206+
break;
11941207
}
1208+
1209+
return -1;
11951210
}
11961211

11971212
/// <summary>
@@ -1203,7 +1218,7 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett
12031218
{
12041219
this.stream = stream;
12051220

1206-
this.ReadFileHeader();
1221+
int colorMapSizeBytes = this.ReadFileHeader();
12071222
this.ReadInfoHeader();
12081223

12091224
// see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517
@@ -1218,7 +1233,6 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett
12181233
this.infoHeader.Height = -this.infoHeader.Height;
12191234
}
12201235

1221-
int colorMapSize = -1;
12221236
int bytesPerColorMapEntry = 4;
12231237

12241238
if (this.infoHeader.ClrUsed == 0)
@@ -1227,35 +1241,38 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett
12271241
|| this.infoHeader.BitsPerPixel == 4
12281242
|| this.infoHeader.BitsPerPixel == 8)
12291243
{
1230-
int colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
1244+
if (colorMapSizeBytes == -1)
1245+
{
1246+
colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
1247+
}
1248+
12311249
int colorCountForBitDepth = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel);
12321250
bytesPerColorMapEntry = colorMapSizeBytes / colorCountForBitDepth;
12331251

12341252
// Edge case for less-than-full-sized palette: bytesPerColorMapEntry should be at least 3.
12351253
bytesPerColorMapEntry = Math.Max(bytesPerColorMapEntry, 3);
1236-
colorMapSize = colorMapSizeBytes;
12371254
}
12381255
}
12391256
else
12401257
{
1241-
colorMapSize = this.infoHeader.ClrUsed * bytesPerColorMapEntry;
1258+
colorMapSizeBytes = this.infoHeader.ClrUsed * bytesPerColorMapEntry;
12421259
}
12431260

12441261
palette = null;
12451262

1246-
if (colorMapSize > 0)
1263+
if (colorMapSizeBytes > 0)
12471264
{
12481265
// Usually the color palette is 1024 byte (256 colors * 4), but the documentation does not mention a size limit.
12491266
// Make sure, that we will not read pass the bitmap offset (starting position of image data).
1250-
if ((this.stream.Position + colorMapSize) > this.fileHeader.Offset)
1267+
if ((this.stream.Position + colorMapSizeBytes) > this.fileHeader.Offset)
12511268
{
12521269
BmpThrowHelper.ThrowImageFormatException(
1253-
$"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSize}' is invalid or the bitmap offset.");
1270+
$"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset.");
12541271
}
12551272

1256-
palette = new byte[colorMapSize];
1273+
palette = new byte[colorMapSizeBytes];
12571274

1258-
this.stream.Read(palette, 0, colorMapSize);
1275+
this.stream.Read(palette, 0, colorMapSizeBytes);
12591276
}
12601277

12611278
this.infoHeader.VerifyDimensions();

src/ImageSharp/Formats/Bmp/BmpFileHeader.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors and contributors.
1+
// Copyright (c) Six Labors and contributors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
@@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
2121
internal readonly struct BmpFileHeader
2222
{
2323
/// <summary>
24-
/// Defines of the data structure in the bitmap file.
24+
/// Defines the size of the data structure in the bitmap file.
2525
/// </summary>
2626
public const int Size = 14;
2727

@@ -69,4 +69,4 @@ public unsafe void WriteTo(Span<byte> buffer)
6969
dest = this;
7070
}
7171
}
72-
}
72+
}

src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors and contributors.
1+
// Copyright (c) Six Labors and contributors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
@@ -22,7 +22,9 @@ public IImageFormat DetectFormat(ReadOnlySpan<byte> header)
2222

2323
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
2424
{
25-
return header.Length >= this.HeaderSize && BinaryPrimitives.ReadInt16LittleEndian(header) == BmpConstants.TypeMarkers.Bitmap;
25+
short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(header);
26+
return header.Length >= this.HeaderSize &&
27+
(fileTypeMarker == BmpConstants.TypeMarkers.Bitmap || fileTypeMarker == BmpConstants.TypeMarkers.BitmapArray);
2628
}
2729
}
28-
}
30+
}

src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors and contributors.
1+
// Copyright (c) Six Labors and contributors.
22
// Licensed under the Apache License, Version 2.0.
33
using System;
44
using System.Buffers.Binary;
@@ -51,10 +51,15 @@ internal struct BmpInfoHeader
5151
/// </summary>
5252
public const int SizeV4 = 108;
5353

54+
/// <summary>
55+
/// Defines the size of the BITMAPINFOHEADER (BMP Version 5) data structure in the bitmap file.
56+
/// </summary>
57+
public const int SizeV5 = 124;
58+
5459
/// <summary>
5560
/// Defines the size of the biggest supported header data structure in the bitmap file.
5661
/// </summary>
57-
public const int MaxHeaderSize = SizeV4;
62+
public const int MaxHeaderSize = SizeV5;
5863

5964
/// <summary>
6065
/// Defines the size of the <see cref="HeaderSize"/> field.
@@ -272,7 +277,7 @@ public BmpInfoHeader(
272277
/// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes).
273278
/// </summary>
274279
/// <param name="data">The data to parse.</param>
275-
/// <returns>Parsed header</returns>
280+
/// <returns>The parsed header.</returns>
276281
/// <seealso href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd183372.aspx"/>
277282
public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data)
278283
{
@@ -289,7 +294,7 @@ public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data)
289294
/// are 4 bytes instead of 2, resulting in 16 bytes total.
290295
/// </summary>
291296
/// <param name="data">The data to parse.</param>
292-
/// <returns>Parsed header</returns>
297+
/// <returns>The parsed header.</returns>
293298
/// <seealso href="https://www.fileformat.info/format/os2bmp/egff.htm"/>
294299
public static BmpInfoHeader ParseOs22Short(ReadOnlySpan<byte> data)
295300
{
@@ -406,7 +411,7 @@ public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan<byte> data)
406411
/// <seealso href="http://www.fileformat.info/format/bmp/egff.htm"/>
407412
public static BmpInfoHeader ParseV4(ReadOnlySpan<byte> data)
408413
{
409-
if (data.Length != SizeV4)
414+
if (data.Length < SizeV4)
410415
{
411416
throw new ArgumentException(nameof(data), $"Must be {SizeV4} bytes. Was {data.Length} bytes.");
412417
}
@@ -457,4 +462,4 @@ internal void VerifyDimensions()
457462
}
458463
}
459464
}
460-
}
465+
}

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

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,6 @@ public void BmpDecoder_ThrowsNotSupportedException_OnUnsupportedBitmaps<TPixel>(
343343
}
344344

345345
[Theory]
346-
[WithFile(Rgba32bf56AdobeV3, PixelTypes.Rgba32)]
347346
[WithFile(Rgb32h52AdobeV3, PixelTypes.Rgba32)]
348347
public void BmpDecoder_CanDecodeAdobeBmpv3<TPixel>(TestImageProvider<TPixel> provider)
349348
where TPixel : struct, IPixel<TPixel>
@@ -355,6 +354,18 @@ public void BmpDecoder_CanDecodeAdobeBmpv3<TPixel>(TestImageProvider<TPixel> pro
355354
}
356355
}
357356

357+
[Theory]
358+
[WithFile(Rgba32bf56AdobeV3, PixelTypes.Rgba32)]
359+
public void BmpDecoder_CanDecodeAdobeBmpv3_WithAlpha<TPixel>(TestImageProvider<TPixel> provider)
360+
where TPixel : struct, IPixel<TPixel>
361+
{
362+
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
363+
{
364+
image.DebugSave(provider);
365+
image.CompareToOriginal(provider, new MagickReferenceDecoder());
366+
}
367+
}
368+
358369
[Theory]
359370
[WithFile(WinBmpv4, PixelTypes.Rgba32)]
360371
public void BmpDecoder_CanDecodeBmpv4<TPixel>(TestImageProvider<TPixel> provider)
@@ -429,12 +440,35 @@ public void BmpDecoder_CanDecode4BytePerEntryPalette<TPixel>(TestImageProvider<T
429440
[InlineData(Bit4, 4)]
430441
[InlineData(Bit1, 1)]
431442
[InlineData(Bit1Pal1, 1)]
432-
public void Identify(string imagePath, int expectedPixelSize)
443+
public void Identify_DetectsCorrectPixelType(string imagePath, int expectedPixelSize)
433444
{
434445
var testFile = TestFile.Create(imagePath);
435446
using (var stream = new MemoryStream(testFile.Bytes, false))
436447
{
437-
Assert.Equal(expectedPixelSize, Image.Identify(stream)?.PixelType?.BitsPerPixel);
448+
IImageInfo imageInfo = Image.Identify(stream);
449+
Assert.NotNull(imageInfo);
450+
Assert.Equal(expectedPixelSize, imageInfo.PixelType?.BitsPerPixel);
451+
}
452+
}
453+
454+
[Theory]
455+
[InlineData(Bit32Rgb, 127, 64)]
456+
[InlineData(Car, 600, 450)]
457+
[InlineData(Bit16, 127, 64)]
458+
[InlineData(Bit16Inverted, 127, 64)]
459+
[InlineData(Bit8, 127, 64)]
460+
[InlineData(Bit8Inverted, 127, 64)]
461+
[InlineData(RLE8, 491, 272)]
462+
[InlineData(RLE8Inverted, 491, 272)]
463+
public void Identify_DetectsCorrectWidthAndHeight(string imagePath, int expectedWidth, int expectedHeight)
464+
{
465+
var testFile = TestFile.Create(imagePath);
466+
using (var stream = new MemoryStream(testFile.Bytes, false))
467+
{
468+
IImageInfo imageInfo = Image.Identify(stream);
469+
Assert.NotNull(imageInfo);
470+
Assert.Equal(expectedWidth, imageInfo.Width);
471+
Assert.Equal(expectedHeight, imageInfo.Height);
438472
}
439473
}
440474

@@ -465,8 +499,7 @@ public void BmpDecoder_CanDecode_Os2v2XShortHeader<TPixel>(TestImageProvider<TPi
465499
{
466500
image.DebugSave(provider);
467501

468-
// TODO: Neither System.Drawing not MagickReferenceDecoder
469-
// can correctly decode this file.
502+
// TODO: Neither System.Drawing or MagickReferenceDecoder can correctly decode this file.
470503
// image.CompareToOriginal(provider);
471504
}
472505
}
@@ -486,5 +519,26 @@ public void BmpDecoder_CanDecode_Os2v2Header<TPixel>(TestImageProvider<TPixel> p
486519
// image.CompareToOriginal(provider, new MagickReferenceDecoder());
487520
}
488521
}
522+
523+
[Theory]
524+
[WithFile(Os2BitmapArray9s, PixelTypes.Rgba32)]
525+
[WithFile(Os2BitmapArrayDiamond, PixelTypes.Rgba32)]
526+
[WithFile(Os2BitmapArraySkater, PixelTypes.Rgba32)]
527+
[WithFile(Os2BitmapArraySpade, PixelTypes.Rgba32)]
528+
[WithFile(Os2BitmapArraySunflower, PixelTypes.Rgba32)]
529+
[WithFile(Os2BitmapArrayMarble, PixelTypes.Rgba32)]
530+
[WithFile(Os2BitmapArrayWarpd, PixelTypes.Rgba32)]
531+
[WithFile(Os2BitmapArrayPines, PixelTypes.Rgba32)]
532+
public void BmpDecoder_CanDecode_Os2BitmapArray<TPixel>(TestImageProvider<TPixel> provider)
533+
where TPixel : struct, IPixel<TPixel>
534+
{
535+
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
536+
{
537+
image.DebugSave(provider);
538+
539+
// TODO: Neither System.Drawing or MagickReferenceDecoder can correctly decode this file.
540+
// image.CompareToOriginal(provider);
541+
}
542+
}
489543
}
490544
}

0 commit comments

Comments
 (0)