Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/ImageSharp/Formats/Bmp/BmpArrayFileHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Runtime.InteropServices;

namespace SixLabors.ImageSharp.Formats.Bmp
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal readonly struct BmpArrayFileHeader
{
public BmpArrayFileHeader(short type, int size, int offsetToNext, short width, short height)
{
this.Type = type;
this.Size = size;
this.OffsetToNext = offsetToNext;
this.ScreenWidth = width;
this.ScreenHeight = height;
}

/// <summary>
/// Gets the Bitmap identifier.
/// The field used to identify the bitmap file: 0x42 0x41 (Hex code points for B and A).
/// </summary>
public short Type { get; }

/// <summary>
/// Gets the size of this header.
/// </summary>
public int Size { get; }

/// <summary>
/// Gets the offset to next OS2BMPARRAYFILEHEADER.
/// 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.
/// </summary>
public int OffsetToNext { get; }

/// <summary>
/// Gets the width of the image display in pixels.
/// </summary>
public short ScreenWidth { get; }

/// <summary>
/// Gets the height of the image display in pixels.
/// </summary>
public short ScreenHeight { get; }

public static BmpArrayFileHeader Parse(Span<byte> data)
{
return MemoryMarshal.Cast<byte, BmpArrayFileHeader>(data)[0];
}
}
}
69 changes: 43 additions & 26 deletions src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1053,18 +1053,11 @@ private void ReadInfoHeader()
this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize);

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

int skipAmount = 0;
if (headerSize > BmpInfoHeader.MaxHeaderSize)
{
skipAmount = headerSize - BmpInfoHeader.MaxHeaderSize;
headerSize = BmpInfoHeader.MaxHeaderSize;
}

// Read the rest of the header.
this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize);

Expand Down Expand Up @@ -1169,15 +1162,13 @@ private void ReadInfoHeader()
{
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
}

// Skip the remaining header because we can't read those parts.
this.stream.Skip(skipAmount);
}

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

this.fileHeader = BmpFileHeader.Parse(buffer);

if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(buffer);
switch (fileTypeMarker)
{
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{this.fileHeader.Type}'.");
case BmpConstants.TypeMarkers.Bitmap:
this.fileHeader = BmpFileHeader.Parse(buffer);
break;
case BmpConstants.TypeMarkers.BitmapArray:
// The Array file header is followed by the bitmap file header of the first image.
var arrayHeader = BmpArrayFileHeader.Parse(buffer);
this.stream.Read(buffer, 0, BmpFileHeader.Size);
this.fileHeader = BmpFileHeader.Parse(buffer);
if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
{
BmpThrowHelper.ThrowNotSupportedException($"Unsupported bitmap file inside a BitmapArray file. File header bitmap type marker '{this.fileHeader.Type}'.");
}

if (arrayHeader.OffsetToNext != 0)
{
int colorMapSizeBytes = arrayHeader.OffsetToNext - arrayHeader.Size;
return colorMapSizeBytes;
}

break;

default:
BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. File header bitmap type marker '{fileTypeMarker}'.");
break;
}

return -1;
}

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

this.ReadFileHeader();
int colorMapSizeBytes = this.ReadFileHeader();
this.ReadInfoHeader();

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

int colorMapSize = -1;
int bytesPerColorMapEntry = 4;

if (this.infoHeader.ClrUsed == 0)
Expand All @@ -1227,35 +1241,38 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett
|| this.infoHeader.BitsPerPixel == 4
|| this.infoHeader.BitsPerPixel == 8)
{
int colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
if (colorMapSizeBytes == -1)
{
colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
}

int colorCountForBitDepth = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel);
bytesPerColorMapEntry = colorMapSizeBytes / colorCountForBitDepth;

// Edge case for less-than-full-sized palette: bytesPerColorMapEntry should be at least 3.
bytesPerColorMapEntry = Math.Max(bytesPerColorMapEntry, 3);
colorMapSize = colorMapSizeBytes;
}
}
else
{
colorMapSize = this.infoHeader.ClrUsed * bytesPerColorMapEntry;
colorMapSizeBytes = this.infoHeader.ClrUsed * bytesPerColorMapEntry;
}

palette = null;

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

palette = new byte[colorMapSize];
palette = new byte[colorMapSizeBytes];

this.stream.Read(palette, 0, colorMapSize);
this.stream.Read(palette, 0, colorMapSizeBytes);
}

this.infoHeader.VerifyDimensions();
Expand Down
6 changes: 3 additions & 3 deletions src/ImageSharp/Formats/Bmp/BmpFileHeader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using System;
Expand All @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
internal readonly struct BmpFileHeader
{
/// <summary>
/// Defines of the data structure in the bitmap file.
/// Defines the size of the data structure in the bitmap file.
/// </summary>
public const int Size = 14;

Expand Down Expand Up @@ -69,4 +69,4 @@ public unsafe void WriteTo(Span<byte> buffer)
dest = this;
}
}
}
}
8 changes: 5 additions & 3 deletions src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

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

private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
{
return header.Length >= this.HeaderSize && BinaryPrimitives.ReadInt16LittleEndian(header) == BmpConstants.TypeMarkers.Bitmap;
short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(header);
return header.Length >= this.HeaderSize &&
(fileTypeMarker == BmpConstants.TypeMarkers.Bitmap || fileTypeMarker == BmpConstants.TypeMarkers.BitmapArray);
}
}
}
}
17 changes: 11 additions & 6 deletions src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
Expand Down Expand Up @@ -51,10 +51,15 @@ internal struct BmpInfoHeader
/// </summary>
public const int SizeV4 = 108;

/// <summary>
/// Defines the size of the BITMAPINFOHEADER (BMP Version 5) data structure in the bitmap file.
/// </summary>
public const int SizeV5 = 124;

/// <summary>
/// Defines the size of the biggest supported header data structure in the bitmap file.
/// </summary>
public const int MaxHeaderSize = SizeV4;
public const int MaxHeaderSize = SizeV5;

/// <summary>
/// Defines the size of the <see cref="HeaderSize"/> field.
Expand Down Expand Up @@ -272,7 +277,7 @@ public BmpInfoHeader(
/// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes).
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>Parsed header</returns>
/// <returns>The parsed header.</returns>
/// <seealso href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd183372.aspx"/>
public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data)
{
Expand All @@ -289,7 +294,7 @@ public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data)
/// are 4 bytes instead of 2, resulting in 16 bytes total.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>Parsed header</returns>
/// <returns>The parsed header.</returns>
/// <seealso href="https://www.fileformat.info/format/os2bmp/egff.htm"/>
public static BmpInfoHeader ParseOs22Short(ReadOnlySpan<byte> data)
{
Expand Down Expand Up @@ -406,7 +411,7 @@ public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan<byte> data)
/// <seealso href="http://www.fileformat.info/format/bmp/egff.htm"/>
public static BmpInfoHeader ParseV4(ReadOnlySpan<byte> data)
{
if (data.Length != SizeV4)
if (data.Length < SizeV4)
{
throw new ArgumentException(nameof(data), $"Must be {SizeV4} bytes. Was {data.Length} bytes.");
}
Expand Down Expand Up @@ -457,4 +462,4 @@ internal void VerifyDimensions()
}
}
}
}
}
64 changes: 59 additions & 5 deletions tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,6 @@ public void BmpDecoder_ThrowsNotSupportedException_OnUnsupportedBitmaps<TPixel>(
}

[Theory]
[WithFile(Rgba32bf56AdobeV3, PixelTypes.Rgba32)]
[WithFile(Rgb32h52AdobeV3, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeAdobeBmpv3<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
Expand All @@ -355,6 +354,18 @@ public void BmpDecoder_CanDecodeAdobeBmpv3<TPixel>(TestImageProvider<TPixel> pro
}
}

[Theory]
[WithFile(Rgba32bf56AdobeV3, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeAdobeBmpv3_WithAlpha<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
{
image.DebugSave(provider);
image.CompareToOriginal(provider, new MagickReferenceDecoder());
}
}

[Theory]
[WithFile(WinBmpv4, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeBmpv4<TPixel>(TestImageProvider<TPixel> provider)
Expand Down Expand Up @@ -429,12 +440,35 @@ public void BmpDecoder_CanDecode4BytePerEntryPalette<TPixel>(TestImageProvider<T
[InlineData(Bit4, 4)]
[InlineData(Bit1, 1)]
[InlineData(Bit1Pal1, 1)]
public void Identify(string imagePath, int expectedPixelSize)
public void Identify_DetectsCorrectPixelType(string imagePath, int expectedPixelSize)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
Assert.Equal(expectedPixelSize, Image.Identify(stream)?.PixelType?.BitsPerPixel);
IImageInfo imageInfo = Image.Identify(stream);
Assert.NotNull(imageInfo);
Assert.Equal(expectedPixelSize, imageInfo.PixelType?.BitsPerPixel);
}
}

[Theory]
[InlineData(Bit32Rgb, 127, 64)]
[InlineData(Car, 600, 450)]
[InlineData(Bit16, 127, 64)]
[InlineData(Bit16Inverted, 127, 64)]
[InlineData(Bit8, 127, 64)]
[InlineData(Bit8Inverted, 127, 64)]
[InlineData(RLE8, 491, 272)]
[InlineData(RLE8Inverted, 491, 272)]
public void Identify_DetectsCorrectWidthAndHeight(string imagePath, int expectedWidth, int expectedHeight)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
IImageInfo imageInfo = Image.Identify(stream);
Assert.NotNull(imageInfo);
Assert.Equal(expectedWidth, imageInfo.Width);
Assert.Equal(expectedHeight, imageInfo.Height);
}
}

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

// TODO: Neither System.Drawing not MagickReferenceDecoder
// can correctly decode this file.
// TODO: Neither System.Drawing or MagickReferenceDecoder can correctly decode this file.
// image.CompareToOriginal(provider);
}
}
Expand All @@ -486,5 +519,26 @@ public void BmpDecoder_CanDecode_Os2v2Header<TPixel>(TestImageProvider<TPixel> p
// image.CompareToOriginal(provider, new MagickReferenceDecoder());
}
}

[Theory]
[WithFile(Os2BitmapArray9s, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArrayDiamond, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArraySkater, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArraySpade, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArraySunflower, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArrayMarble, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArrayWarpd, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArrayPines, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_Os2BitmapArray<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
{
image.DebugSave(provider);

// TODO: Neither System.Drawing or MagickReferenceDecoder can correctly decode this file.
// image.CompareToOriginal(provider);
}
}
}
}
Loading