From 6fcfe09c388de30459b22f402e649d72745958c6 Mon Sep 17 00:00:00 2001
From: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Date: Mon, 10 Jun 2019 08:24:54 +0200
Subject: [PATCH] Feature: Bitmap RLE undefined pixel handling (#927)
* Add bitmap decoder option, how to treat skipped pixels for RLE
* Refactored bitmap tests into smaller tests, instead of just one test which goes through all bitmap files
* Add another adobe v3 header bitmap testcase
* Using the constant from BmpConstants to Identify bitmaps
* Bitmap decoder now can handle oversized palette's
* Add test for invalid palette size
* Renamed RleUndefinedPixelHandling to RleSkippedPixelHandling
* Explicitly using SystemDrawingReferenceDecoder in some BitmapDecoder tests
* Add test cases for unsupported bitmaps
* Comparing RLE test images to reference decoder only on windows
* Add test case for decoding winv4 fast path
* Add another 8 Bit RLE test with magick reference decoder
* Optimize RLE skipped pixel handling
* Refactor RLE decoding to eliminate code duplication
* Using MagickReferenceDecoder for the 8-Bit RLE test
---
src/ImageSharp/Formats/Bmp/BmpDecoder.cs | 7 +-
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 240 ++++++++++++++----
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 9 +
.../Formats/Bmp/BmpImageFormatDetector.cs | 6 +-
.../Formats/Bmp/IBmpDecoderOptions.cs | 5 +-
.../Formats/Bmp/RleSkippedPixelHandling.cs | 26 ++
tests/ImageSharp.Tests/FileTestBase.cs | 2 +-
.../Formats/Bmp/BmpDecoderTests.cs | 226 ++++++++++++++++-
.../JpegProfilingBenchmarks.cs | 2 +-
tests/ImageSharp.Tests/TestImages.cs | 57 +++--
tests/Images/Input/Bmp/invalidPaletteSize.bmp | Bin 0 -> 9270 bytes
tests/Images/Input/Bmp/pal4rlecut.bmp | Bin 0 -> 3610 bytes
tests/Images/Input/Bmp/pal4rletrns.bmp | Bin 0 -> 4326 bytes
tests/Images/Input/Bmp/pal8oversizepal.bmp | Bin 0 -> 9446 bytes
tests/Images/Input/Bmp/pal8rlecut.bmp | Bin 0 -> 7980 bytes
tests/Images/Input/Bmp/pal8rletrns.bmp | Bin 0 -> 9212 bytes
tests/Images/Input/Bmp/rgb24jpeg.bmp | Bin 0 -> 2457 bytes
tests/Images/Input/Bmp/rgb24largepal.bmp | Bin 0 -> 25830 bytes
tests/Images/Input/Bmp/rgb24png.bmp | Bin 0 -> 1210 bytes
tests/Images/Input/Bmp/rgb32h52.bmp | Bin 0 -> 32578 bytes
tests/Images/Input/Bmp/rgba32v4.bmp | Bin 0 -> 32634 bytes
tests/Images/Input/Bmp/rle4-delta-320x240.bmp | Bin 0 -> 3686 bytes
tests/Images/Input/Bmp/rle8-blank-160x120.bmp | Bin 0 -> 1080 bytes
tests/Images/Input/Bmp/rle8-delta-320x240.bmp | Bin 0 -> 4646 bytes
24 files changed, 495 insertions(+), 85 deletions(-)
create mode 100644 src/ImageSharp/Formats/Bmp/RleSkippedPixelHandling.cs
create mode 100644 tests/Images/Input/Bmp/invalidPaletteSize.bmp
create mode 100644 tests/Images/Input/Bmp/pal4rlecut.bmp
create mode 100644 tests/Images/Input/Bmp/pal4rletrns.bmp
create mode 100644 tests/Images/Input/Bmp/pal8oversizepal.bmp
create mode 100644 tests/Images/Input/Bmp/pal8rlecut.bmp
create mode 100644 tests/Images/Input/Bmp/pal8rletrns.bmp
create mode 100644 tests/Images/Input/Bmp/rgb24jpeg.bmp
create mode 100644 tests/Images/Input/Bmp/rgb24largepal.bmp
create mode 100644 tests/Images/Input/Bmp/rgb24png.bmp
create mode 100644 tests/Images/Input/Bmp/rgb32h52.bmp
create mode 100644 tests/Images/Input/Bmp/rgba32v4.bmp
create mode 100644 tests/Images/Input/Bmp/rle4-delta-320x240.bmp
create mode 100644 tests/Images/Input/Bmp/rle8-blank-160x120.bmp
create mode 100644 tests/Images/Input/Bmp/rle8-delta-320x240.bmp
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs
index ebb7ffdf3c..a404ab418b 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs
@@ -14,13 +14,18 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// - JPG
/// - PNG
- /// - RLE4
+ /// - Some OS/2 specific subtypes like: Bitmap Array, Color Icon, Color Pointer, Icon, Pointer.
///
/// Formats will be supported in a later releases. We advise always
/// to use only 24 Bit Windows bitmaps.
///
public sealed class BmpDecoder : IImageDecoder, IBmpDecoderOptions, IImageInfoDetector
{
+ ///
+ /// Gets or sets a value indicating how to deal with skipped pixels, which can occur during decoding run length encoded bitmaps.
+ ///
+ public RleSkippedPixelHandling RleSkippedPixelHandling { get; set; } = RleSkippedPixelHandling.Black;
+
///
public Image Decode(Configuration configuration, Stream stream)
where TPixel : struct, IPixel
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 0cbc4fca1b..294b49ed7e 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -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;
@@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private ImageMetadata metadata;
///
- /// The bmp specific metadata.
+ /// The bitmap specific metadata.
///
private BmpMetadata bmpMetadata;
@@ -83,10 +83,21 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
private BmpInfoHeader infoHeader;
+ ///
+ /// The global configuration.
+ ///
private readonly Configuration configuration;
+ ///
+ /// Used for allocating memory during processing operations.
+ ///
private readonly MemoryAllocator memoryAllocator;
+ ///
+ /// The bitmap decoder options.
+ ///
+ private readonly IBmpDecoderOptions options;
+
///
/// Initializes a new instance of the class.
///
@@ -96,6 +107,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
{
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
+ this.options = options;
}
///
@@ -207,7 +219,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// The image width.
/// The pixel component count.
///
- /// The .
+ /// The padding.
///
private static int CalculatePadding(int width, int componentCount)
{
@@ -222,7 +234,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
///
- /// Decodes a bitmap containing BITFIELDS Compression type. For each color channel, there will be bitmask
+ /// Decodes a bitmap containing the BITFIELDS Compression type. For each color channel, there will be a bitmask
/// which will be used to determine which bits belong to that channel.
///
/// The pixel format.
@@ -258,8 +270,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Looks up color values and builds the image from de-compressed RLE8 or RLE4 data.
- /// Compressed RLE8 stream is uncompressed by
- /// Compressed RLE4 stream is uncompressed by
+ /// Compressed RLE8 stream is uncompressed by
+ /// Compressed RLE4 stream is uncompressed by
///
/// The pixel format.
/// The compression type. Either RLE4 or RLE8.
@@ -273,14 +285,17 @@ namespace SixLabors.ImageSharp.Formats.Bmp
{
TPixel color = default;
using (Buffer2D buffer = this.memoryAllocator.Allocate2D(width, height, AllocationOptions.Clean))
+ using (Buffer2D undefinedPixels = this.memoryAllocator.Allocate2D(width, height, AllocationOptions.Clean))
+ using (IMemoryOwner rowsWithUndefinedPixels = this.memoryAllocator.Allocate(height, AllocationOptions.Clean))
{
+ Span rowsWithUndefinedPixelsSpan = rowsWithUndefinedPixels.Memory.Span;
if (compression == BmpCompression.RLE8)
{
- this.UncompressRle8(width, buffer.GetSpan());
+ this.UncompressRle8(width, buffer.GetSpan(), undefinedPixels.GetSpan(), rowsWithUndefinedPixelsSpan);
}
else
{
- this.UncompressRle4(width, buffer.GetSpan());
+ this.UncompressRle4(width, buffer.GetSpan(), undefinedPixels.GetSpan(), rowsWithUndefinedPixelsSpan);
}
for (int y = 0; y < height; y++)
@@ -289,10 +304,46 @@ namespace SixLabors.ImageSharp.Formats.Bmp
Span bufferRow = buffer.GetRowSpan(y);
Span pixelRow = pixels.GetRowSpan(newY);
- for (int x = 0; x < width; x++)
+ bool rowHasUndefinedPixels = rowsWithUndefinedPixelsSpan[y];
+ if (rowHasUndefinedPixels)
{
- color.FromBgr24(Unsafe.As(ref colors[bufferRow[x] * 4]));
- pixelRow[x] = color;
+ // Slow path with undefined pixels.
+ for (int x = 0; x < width; x++)
+ {
+ byte colorIdx = bufferRow[x];
+ if (undefinedPixels[x, y])
+ {
+ switch (this.options.RleSkippedPixelHandling)
+ {
+ case RleSkippedPixelHandling.FirstColorOfPalette:
+ color.FromBgr24(Unsafe.As(ref colors[colorIdx * 4]));
+ break;
+ case RleSkippedPixelHandling.Transparent:
+ color.FromVector4(Vector4.Zero);
+ break;
+
+ // Default handling for skipped pixels is black (which is what System.Drawing is also doing).
+ default:
+ color.FromVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f));
+ break;
+ }
+ }
+ else
+ {
+ color.FromBgr24(Unsafe.As(ref colors[colorIdx * 4]));
+ }
+
+ pixelRow[x] = color;
+ }
+ }
+ else
+ {
+ // Fast path without any undefined pixels.
+ for (int x = 0; x < width; x++)
+ {
+ color.FromBgr24(Unsafe.As(ref colors[bufferRow[x] * 4]));
+ pixelRow[x] = color;
+ }
}
}
}
@@ -308,7 +359,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// The width of the bitmap.
/// Buffer for uncompressed data.
- private void UncompressRle4(int w, Span buffer)
+ /// Keeps track over skipped and therefore undefined pixels.
+ /// Keeps track of rows, which have undefined pixels.
+ private void UncompressRle4(int w, Span buffer, Span undefinedPixels, Span rowsWithUndefinedPixels)
{
#if NETCOREAPP2_1
Span cmd = stackalloc byte[2];
@@ -329,21 +382,20 @@ namespace SixLabors.ImageSharp.Formats.Bmp
switch (cmd[1])
{
case RleEndOfBitmap:
+ int skipEoB = buffer.Length - count;
+ RleSkipEndOfBitmap(count, w, skipEoB, undefinedPixels, rowsWithUndefinedPixels);
+
return;
case RleEndOfLine:
- int extra = count % w;
- if (extra > 0)
- {
- count += w - extra;
- }
+ count += RleSkipEndOfLine(count, w, undefinedPixels, rowsWithUndefinedPixels);
break;
case RleDelta:
int dx = this.stream.ReadByte();
int dy = this.stream.ReadByte();
- count += (w * dy) + dx;
+ count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels);
break;
@@ -374,7 +426,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
}
- // Absolute mode data is aligned to two-byte word-boundary
+ // Absolute mode data is aligned to two-byte word-boundary.
int padding = bytesToRead & 1;
this.stream.Skip(padding);
@@ -418,7 +470,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// The width of the bitmap.
/// Buffer for uncompressed data.
- private void UncompressRle8(int w, Span buffer)
+ /// Keeps track of skipped and therefore undefined pixels.
+ /// Keeps track of rows, which have undefined pixels.
+ private void UncompressRle8(int w, Span buffer, Span undefinedPixels, Span rowsWithUndefinedPixels)
{
#if NETCOREAPP2_1
Span cmd = stackalloc byte[2];
@@ -439,27 +493,26 @@ namespace SixLabors.ImageSharp.Formats.Bmp
switch (cmd[1])
{
case RleEndOfBitmap:
+ int skipEoB = buffer.Length - count;
+ RleSkipEndOfBitmap(count, w, skipEoB, undefinedPixels, rowsWithUndefinedPixels);
+
return;
case RleEndOfLine:
- int extra = count % w;
- if (extra > 0)
- {
- count += w - extra;
- }
+ count += RleSkipEndOfLine(count, w, undefinedPixels, rowsWithUndefinedPixels);
break;
case RleDelta:
int dx = this.stream.ReadByte();
int dy = this.stream.ReadByte();
- count += (w * dy) + dx;
+ count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels);
break;
default:
- // If the second byte > 2, we are in 'absolute mode'
- // Take this number of bytes from the stream as uncompressed data
+ // If the second byte > 2, we are in 'absolute mode'.
+ // Take this number of bytes from the stream as uncompressed data.
int length = cmd[1];
byte[] run = new byte[length];
@@ -470,7 +523,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
count += run.Length;
- // Absolute mode data is aligned to two-byte word-boundary
+ // Absolute mode data is aligned to two-byte word-boundary.
int padding = length & 1;
this.stream.Skip(padding);
@@ -481,16 +534,105 @@ namespace SixLabors.ImageSharp.Formats.Bmp
else
{
int max = count + cmd[0]; // as we start at the current count in the following loop, max is count + cmd[0]
- byte cmd1 = cmd[1]; // store the value to avoid the repeated indexer access inside the loop
+ byte colorIdx = cmd[1]; // store the value to avoid the repeated indexer access inside the loop.
for (; count < max; count++)
{
- buffer[count] = cmd1;
+ buffer[count] = colorIdx;
}
}
}
}
+ ///
+ /// Keeps track of skipped / undefined pixels, when EndOfBitmap command occurs.
+ ///
+ /// The already processed pixel count.
+ /// The width of the image.
+ /// The skipped pixel count.
+ /// The undefined pixels.
+ /// Rows with undefined pixels.
+ private static void RleSkipEndOfBitmap(
+ int count,
+ int w,
+ int skipPixelCount,
+ Span undefinedPixels,
+ Span rowsWithUndefinedPixels)
+ {
+ for (int i = count; i < count + skipPixelCount; i++)
+ {
+ undefinedPixels[i] = true;
+ }
+
+ int skippedRowIdx = count / w;
+ int skippedRows = (skipPixelCount / w) - 1;
+ int lastSkippedRow = Math.Min(skippedRowIdx + skippedRows, rowsWithUndefinedPixels.Length - 1);
+ for (int i = skippedRowIdx; i <= lastSkippedRow; i++)
+ {
+ rowsWithUndefinedPixels[i] = true;
+ }
+ }
+
+ ///
+ /// Keeps track of undefined / skipped pixels, when the EndOfLine command occurs.
+ ///
+ /// The already processed pixel count.
+ /// The width of image.
+ /// The undefined pixels.
+ /// The rows with undefined pixels.
+ /// The number of skipped pixels.
+ private static int RleSkipEndOfLine(int count, int w, Span undefinedPixels, Span rowsWithUndefinedPixels)
+ {
+ rowsWithUndefinedPixels[count / w] = true;
+ int remainingPixelsInRow = count % w;
+ if (remainingPixelsInRow > 0)
+ {
+ int skipEoL = w - remainingPixelsInRow;
+ for (int i = count; i < count + skipEoL; i++)
+ {
+ undefinedPixels[i] = true;
+ }
+
+ return skipEoL;
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Keeps track of undefined / skipped pixels, when the delta command occurs.
+ ///
+ /// The count.
+ /// The width of the image.
+ /// Delta skip in x direction.
+ /// Delta skip in y direction.
+ /// The undefined pixels.
+ /// The rows with undefined pixels.
+ /// The number of skipped pixels.
+ private static int RleSkipDelta(
+ int count,
+ int w,
+ int dx,
+ int dy,
+ Span undefinedPixels,
+ Span rowsWithUndefinedPixels)
+ {
+ int skipDelta = (w * dy) + dx;
+ for (int i = count; i < count + skipDelta; i++)
+ {
+ undefinedPixels[i] = true;
+ }
+
+ int skippedRowIdx = count / w;
+ int lastSkippedRow = Math.Min(skippedRowIdx + dy, rowsWithUndefinedPixels.Length - 1);
+ for (int i = skippedRowIdx; i <= lastSkippedRow; i++)
+ {
+ rowsWithUndefinedPixels[i] = true;
+ }
+
+ return skipDelta;
+ }
+
///
/// Reads the color palette from the stream.
///
@@ -506,7 +648,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private void ReadRgbPalette(Buffer2D pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted)
where TPixel : struct, IPixel
{
- // Pixels per byte (bits per pixel)
+ // Pixels per byte (bits per pixel).
int ppb = 8 / bitsPerPixel;
int arrayWidth = (width + ppb - 1) / ppb;
@@ -514,7 +656,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
// Bit mask
int mask = 0xFF >> (8 - bitsPerPixel);
- // Rows are aligned on 4 byte boundaries
+ // Rows are aligned on 4 byte boundaries.
int padding = arrayWidth % 4;
if (padding != 0)
{
@@ -810,11 +952,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
uint maxValueAlpha = 0xFFFFFFFF >> (32 - bitsAlphaMask);
float invMaxValueAlpha = 1.0f / maxValueAlpha;
- bool unusualBitMask = false;
- if (bitsRedMask > 8 || bitsGreenMask > 8 || bitsBlueMask > 8 || invMaxValueAlpha > 8)
- {
- unusualBitMask = true;
- }
+ bool unusualBitMask = bitsRedMask > 8 || bitsGreenMask > 8 || bitsBlueMask > 8 || invMaxValueAlpha > 8;
using (IManagedByteBuffer buffer = this.memoryAllocator.AllocateManagedByteBuffer(stride))
{
@@ -910,7 +1048,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp
#else
byte[] buffer = new byte[BmpInfoHeader.MaxHeaderSize];
#endif
- this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize); // read the header size
+
+ // Read the header size.
+ this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize);
int headerSize = BinaryPrimitives.ReadInt32LittleEndian(buffer);
if (headerSize < BmpInfoHeader.CoreSize)
@@ -925,7 +1065,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
headerSize = BmpInfoHeader.MaxHeaderSize;
}
- // read the rest of the header
+ // Read the rest of the header.
this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize);
BmpInfoHeaderType infoHeaderType = BmpInfoHeaderType.WinVersion2;
@@ -953,7 +1093,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
{
byte[] bitfieldsBuffer = new byte[12];
this.stream.Read(bitfieldsBuffer, 0, 12);
- Span data = bitfieldsBuffer.AsSpan();
+ Span data = bitfieldsBuffer.AsSpan();
this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4));
this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4));
@@ -962,7 +1102,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
{
byte[] bitfieldsBuffer = new byte[16];
this.stream.Read(bitfieldsBuffer, 0, 16);
- Span data = bitfieldsBuffer.AsSpan();
+ Span data = bitfieldsBuffer.AsSpan();
this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4));
this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
this.infoHeader.BlueMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4));
@@ -1021,7 +1161,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.bmpMetadata = this.metadata.GetFormatMetadata(BmpFormat.Instance);
this.bmpMetadata.InfoHeaderType = infoHeaderType;
- // We can only encode at these bit rates so far.
+ // We can only encode at these bit rates so far (1 bit and 4 bit are still missing).
if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel8)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel16)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24)
@@ -1030,7 +1170,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
}
- // skip the remaining header because we can't read those parts
+ // Skip the remaining header because we can't read those parts.
this.stream.Skip(skipAmount);
}
@@ -1105,10 +1245,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
if (colorMapSize > 0)
{
- // 256 * 4
- if (colorMapSize > 1024)
+ // 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)
{
- BmpThrowHelper.ThrowImageFormatException($"Invalid bmp colormap size '{colorMapSize}'");
+ 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.");
}
palette = new byte[colorMapSize];
@@ -1121,7 +1263,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int skipAmount = this.fileHeader.Offset - (int)this.stream.Position;
if ((skipAmount + (int)this.stream.Position) > this.stream.Length)
{
- BmpThrowHelper.ThrowImageFormatException($"Invalid fileheader offset found. Offset is greater than the stream length.");
+ BmpThrowHelper.ThrowImageFormatException("Invalid fileheader offset found. Offset is greater than the stream length.");
}
if (skipAmount > 0)
@@ -1132,4 +1274,4 @@ namespace SixLabors.ImageSharp.Formats.Bmp
return bytesPerColorMapEntry;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 9fbd0b5adb..4e6ec45021 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -51,10 +51,19 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
private const int ColorPaletteSize8Bit = 1024;
+ ///
+ /// Used for allocating memory during processing operations.
+ ///
private readonly MemoryAllocator memoryAllocator;
+ ///
+ /// The global configuration.
+ ///
private Configuration configuration;
+ ///
+ /// The color depth, in number of bits per pixel.
+ ///
private BmpBitsPerPixel? bitsPerPixel;
///
diff --git a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
index c0814b1dfc..4f862d9295 100644
--- a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Bmp
{
@@ -21,10 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private bool IsSupportedFileFormat(ReadOnlySpan header)
{
- // TODO: This should be in constants
- return header.Length >= this.HeaderSize
- && header[0] == 0x42 // B
- && header[1] == 0x4D; // M
+ return header.Length >= this.HeaderSize && BinaryPrimitives.ReadInt16LittleEndian(header) == BmpConstants.TypeMarkers.Bitmap;
}
}
}
\ No newline at end of file
diff --git a/src/ImageSharp/Formats/Bmp/IBmpDecoderOptions.cs b/src/ImageSharp/Formats/Bmp/IBmpDecoderOptions.cs
index 219d37ca62..f456f2ba3a 100644
--- a/src/ImageSharp/Formats/Bmp/IBmpDecoderOptions.cs
+++ b/src/ImageSharp/Formats/Bmp/IBmpDecoderOptions.cs
@@ -8,6 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
internal interface IBmpDecoderOptions
{
- // added this for consistency so we can add stuff as required, no options currently available
+ ///
+ /// Gets the value indicating how to deal with skipped pixels, which can occur during decoding run length encoded bitmaps.
+ ///
+ RleSkippedPixelHandling RleSkippedPixelHandling { get; }
}
}
\ No newline at end of file
diff --git a/src/ImageSharp/Formats/Bmp/RleSkippedPixelHandling.cs b/src/ImageSharp/Formats/Bmp/RleSkippedPixelHandling.cs
new file mode 100644
index 0000000000..493fe366ad
--- /dev/null
+++ b/src/ImageSharp/Formats/Bmp/RleSkippedPixelHandling.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.Bmp
+{
+ ///
+ /// Defines possible options, how skipped pixels during decoding of run length encoded bitmaps should be treated.
+ ///
+ public enum RleSkippedPixelHandling : int
+ {
+ ///
+ /// Undefined pixels should be black. This is the default behavior and equal to how System.Drawing handles undefined pixels.
+ ///
+ Black = 0,
+
+ ///
+ /// Undefined pixels should be transparent.
+ ///
+ Transparent = 1,
+
+ ///
+ /// Undefined pixels should have the first color of the palette.
+ ///
+ FirstColorOfPalette = 2
+ }
+}
diff --git a/tests/ImageSharp.Tests/FileTestBase.cs b/tests/ImageSharp.Tests/FileTestBase.cs
index a056bc474e..4f8475738b 100644
--- a/tests/ImageSharp.Tests/FileTestBase.cs
+++ b/tests/ImageSharp.Tests/FileTestBase.cs
@@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Tests
///
/// A collection of all the bmp test images
///
- public static IEnumerable AllBmpFiles = TestImages.Bmp.All;
+ public static IEnumerable AllBmpFiles = TestImages.Bmp.Benchmark;
///
/// A collection of all the jpeg test images
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index e615dbe568..c4dfa724cc 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
@@ -1,6 +1,7 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
+using System;
using System.IO;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.PixelFormats;
@@ -20,27 +21,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
{
public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector;
- public static readonly string[] AllBmpFiles = All;
+ public static readonly string[] MiscBmpFiles = Miscellaneous;
public static readonly string[] BitfieldsBmpFiles = BitFields;
public static readonly TheoryData RatioFiles =
new TheoryData
{
- { TestImages.Bmp.Car, 3780, 3780 , PixelResolutionUnit.PixelsPerMeter },
- { TestImages.Bmp.V5Header, 3780, 3780 , PixelResolutionUnit.PixelsPerMeter },
- { TestImages.Bmp.RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter }
+ { Car, 3780, 3780 , PixelResolutionUnit.PixelsPerMeter },
+ { V5Header, 3780, 3780 , PixelResolutionUnit.PixelsPerMeter },
+ { RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter }
};
[Theory]
- [WithFileCollection(nameof(AllBmpFiles), PixelTypes.Rgba32)]
- public void DecodeBmp(TestImageProvider provider)
+ [WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_MiscellaneousBitmaps(TestImageProvider provider)
where TPixel : struct, IPixel
{
using (Image image = provider.GetImage(new BmpDecoder()))
{
image.DebugSave(provider);
-
if (TestEnvironment.IsWindows)
{
image.CompareToOriginal(provider);
@@ -60,6 +60,174 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
}
}
+ [Theory]
+ [WithFile(Bit16Inverted, PixelTypes.Rgba32)]
+ [WithFile(Bit8Inverted, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_Inverted(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(Bit1, PixelTypes.Rgba32)]
+ [WithFile(Bit1Pal1, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_1Bit(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder());
+ }
+ }
+
+ [Theory]
+ [WithFile(Bit4, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_4Bit(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ // The Magick Reference Decoder can not decode 4-Bit bitmaps, so only execute this on windows.
+ if (TestEnvironment.IsWindows)
+ {
+ image.CompareToOriginal(provider);
+ }
+ }
+ }
+
+ [Theory]
+ [WithFile(Bit8, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_8Bit(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(Bit16, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_16Bit(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(Bit32Rgb, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_32Bit(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(Rgba32v4, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_32BitV4Header_Fast(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
+ [Theory]
+ [WithFile(RLE4Cut, PixelTypes.Rgba32)]
+ [WithFile(RLE4Delta, PixelTypes.Rgba32)]
+ [WithFile(Rle4Delta320240, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.Black }))
+ {
+ image.DebugSave(provider);
+ // The Magick Reference Decoder can not decode 4-Bit bitmaps, so only execute this on windows.
+ if (TestEnvironment.IsWindows)
+ {
+ image.CompareToOriginal(provider);
+ }
+ }
+ }
+
+ [Theory]
+ [WithFile(RLE4, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_RunLengthEncoded_4Bit(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.Black }))
+ {
+ image.DebugSave(provider);
+ // The Magick Reference Decoder can not decode 4-Bit bitmaps, so only execute this on windows.
+ if (TestEnvironment.IsWindows)
+ {
+ image.CompareToOriginal(provider);
+ }
+ }
+ }
+
+ [Theory]
+ [WithFile(RLE8Cut, PixelTypes.Rgba32)]
+ [WithFile(RLE8Delta, PixelTypes.Rgba32)]
+ [WithFile(Rle8Delta320240, PixelTypes.Rgba32)]
+ [WithFile(Rle8Blank160120, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_SystemDrawingRefDecoder(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.Black }))
+ {
+ image.DebugSave(provider);
+ if (TestEnvironment.IsWindows)
+ {
+ image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder());
+ }
+ }
+ }
+
+ [Theory]
+ [WithFile(RLE8Cut, PixelTypes.Rgba32)]
+ [WithFile(RLE8Delta, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_MagickRefDecoder(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette }))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, new MagickReferenceDecoder());
+ }
+ }
+
+ [Theory]
+ [WithFile(RLE8, PixelTypes.Rgba32)]
+ [WithFile(RLE8Inverted, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette }))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, new MagickReferenceDecoder());
+ }
+ }
+
[Theory]
[WithFile(RgbaAlphaBitfields, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeAlphaBitfields(TestImageProvider provider)
@@ -106,6 +274,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
[Theory]
[WithFile(WinBmpv2, PixelTypes.Rgba32)]
+ [WithFile(CoreHeader, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeBmpv2(TestImageProvider provider)
where TPixel : struct, IPixel
{
@@ -141,7 +310,41 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
}
[Theory]
- [WithFile(Rgba32bf56, PixelTypes.Rgba32)]
+ [WithFile(OversizedPalette, PixelTypes.Rgba32)]
+ [WithFile(Rgb24LargePalette, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecodeOversizedPalette(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder()))
+ {
+ image.DebugSave(provider);
+ if (TestEnvironment.IsWindows)
+ {
+ image.CompareToOriginal(provider);
+ }
+ }
+ }
+
+ [Theory]
+ [WithFile(InvalidPaletteSize, PixelTypes.Rgba32)]
+ public void BmpDecoder_ThrowsImageFormatException_OnInvalidPaletteSize(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ Assert.Throws( () => { using (Image image = provider.GetImage(new BmpDecoder())) { } });
+ }
+
+ [Theory]
+ [WithFile(Rgb24jpeg, PixelTypes.Rgba32)]
+ [WithFile(Rgb24png, PixelTypes.Rgba32)]
+ public void BmpDecoder_ThrowsNotSupportedException_OnUnsupportedBitmaps(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ Assert.Throws(() => { using (Image image = provider.GetImage(new BmpDecoder())) { } });
+ }
+
+ [Theory]
+ [WithFile(Rgba32bf56AdobeV3, PixelTypes.Rgba32)]
+ [WithFile(Rgb32h52AdobeV3, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeAdobeBmpv3(TestImageProvider provider)
where TPixel : struct, IPixel
{
@@ -166,6 +369,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
[Theory]
[WithFile(WinBmpv5, PixelTypes.Rgba32)]
+ [WithFile(V5Header, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeBmpv5(TestImageProvider provider)
where TPixel : struct, IPixel
{
@@ -223,6 +427,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
[InlineData(Bit8, 8)]
[InlineData(Bit8Inverted, 8)]
[InlineData(Bit4, 4)]
+ [InlineData(Bit1, 1)]
+ [InlineData(Bit1Pal1, 1)]
public void Identify(string imagePath, int expectedPixelSize)
{
var testFile = TestFile.Create(imagePath);
@@ -281,4 +487,4 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
}
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
index 65989556d2..95a47fd7cc 100644
--- a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
+++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
@@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
return;
}
- string[] testFiles = TestImages.Bmp.All
+ string[] testFiles = TestImages.Bmp.Benchmark
.Concat(new[] { TestImages.Jpeg.Baseline.Calliphora, TestImages.Jpeg.Baseline.Cmyk }).ToArray();
Image[] testImages = testFiles.Select(
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index d8e8719ba1..d041f48544 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -231,8 +231,15 @@ namespace SixLabors.ImageSharp.Tests
public const string CoreHeader = "Bmp/BitmapCoreHeaderQR.bmp";
public const string V5Header = "Bmp/BITMAPV5HEADER.bmp";
public const string RLE8 = "Bmp/RunLengthEncoded.bmp";
+ public const string RLE8Cut = "Bmp/pal8rlecut.bmp";
+ public const string RLE8Delta = "Bmp/pal8rletrns.bmp";
+ public const string Rle8Delta320240 = "Bmp/rle8-delta-320x240.bmp";
+ public const string Rle8Blank160120 = "Bmp/rle8-blank-160x120.bmp";
+ public const string RLE8Inverted = "Bmp/RunLengthEncoded-inverted.bmp";
public const string RLE4 = "Bmp/pal4rle.bmp";
- public const string RLEInverted = "Bmp/RunLengthEncoded-inverted.bmp";
+ public const string RLE4Cut = "Bmp/pal4rlecut.bmp";
+ public const string RLE4Delta = "Bmp/pal4rletrns.bmp";
+ public const string Rle4Delta320240 = "Bmp/rle4-delta-320x240.bmp";
public const string Bit1 = "Bmp/pal1.bmp";
public const string Bit1Pal1 = "Bmp/pal1p1.bmp";
public const string Bit4 = "Bmp/pal4.bmp";
@@ -256,15 +263,22 @@ namespace SixLabors.ImageSharp.Tests
public const string Os2v2 = "Bmp/pal8os2v2.bmp";
public const string LessThanFullSizedPalette = "Bmp/pal8os2sp.bmp";
public const string Pal8Offset = "Bmp/pal8offs.bmp";
+ public const string OversizedPalette = "Bmp/pal8oversizepal.bmp";
+ public const string Rgb24LargePalette = "Bmp/rgb24largepal.bmp";
+ public const string InvalidPaletteSize = "Bmp/invalidPaletteSize.bmp";
+ public const string Rgb24jpeg = "Bmp/rgb24jpeg.bmp";
+ public const string Rgb24png = "Bmp/rgb24png.bmp";
+ public const string Rgba32v4 = "Bmp/rgba32v4.bmp";
- // Bitmap images with compression type BITFIELDS
+ // Bitmap images with compression type BITFIELDS.
public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp";
public const string Rgb32bf = "Bmp/rgb32bf.bmp";
public const string Rgb16bfdef = "Bmp/rgb16bfdef.bmp";
public const string Rgb16565 = "Bmp/rgb16-565.bmp";
public const string Rgb16565pal = "Bmp/rgb16-565pal.bmp";
public const string Issue735 = "Bmp/issue735.bmp";
- public const string Rgba32bf56 = "Bmp/rgba32h56.bmp";
+ public const string Rgba32bf56AdobeV3 = "Bmp/rgba32h56.bmp";
+ public const string Rgb32h52AdobeV3 = "Bmp/rgb32h52.bmp";
public const string Rgba321010102 = "Bmp/rgba32-1010102.bmp";
public const string RgbaAlphaBitfields = "Bmp/rgba32abf.bmp";
@@ -278,25 +292,32 @@ namespace SixLabors.ImageSharp.Tests
Issue735,
};
- public static readonly string[] All
+ public static readonly string[] Miscellaneous
= {
Car,
F,
- NegHeight,
- CoreHeader,
- V5Header,
- RLE4,
- RLE8,
- RLEInverted,
- Bit1,
- Bit1Pal1,
- Bit4,
- Bit8,
- Bit8Inverted,
- Bit16,
- Bit16Inverted,
- Bit32Rgb
+ NegHeight
};
+
+ public static readonly string[] Benchmark
+ = {
+ Car,
+ F,
+ NegHeight,
+ CoreHeader,
+ V5Header,
+ RLE4,
+ RLE8,
+ RLE8Inverted,
+ Bit1,
+ Bit1Pal1,
+ Bit4,
+ Bit8,
+ Bit8Inverted,
+ Bit16,
+ Bit16Inverted,
+ Bit32Rgb
+ };
}
public static class Gif
diff --git a/tests/Images/Input/Bmp/invalidPaletteSize.bmp b/tests/Images/Input/Bmp/invalidPaletteSize.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..afb120bbf956e58ac817c15b379cf9076ccd6678
GIT binary patch
literal 9270
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
literal 0
HcmV?d00001
diff --git a/tests/Images/Input/Bmp/pal4rlecut.bmp b/tests/Images/Input/Bmp/pal4rlecut.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..2f32d1d7add527b6d6d80c4629aadd24f2437c9d
GIT binary patch
literal 3610
zcmb_e&ubf35FYPp-%3`jYP7Ls$!TM$ftHd(mbDF%7aB!Al@X+S^gx2FHJ-vOJH{Unk%+sA8$~lVv
zT6p~$<7bT97;}^&1Mf!*L{3%@Oyj>ZdNQ8H(y9c>VL>YL=h?m1b?1fZ&
zuT9q8XuY}hkgRRG?m07-M{EwW%N@7lb`Rz^$vi$vGudr!=X1-nwAW^buR5?bn%|)6
zc1up4SP2dUfhA{q1G0}D#@XB^J4QiSH})(zGE8PWUsx6_vfgkfXi8IfG#YrE?|K>!
zGtI;27?C_{dY56U08@aIqXTP`HlxAuu`4TLZLw#2kK!E-H@aha1?@Y+}a-effSGMxwwcG+mFfKu&eYC2b{Rdxu^Ix3JsW~cXQ*;1b2OvYTSznNU6C0ELYpplHcoYl4h
z=R{^_=cV}?EI1rY*OJlt;zEXK)xlxf%FFx9`{%0YbI<5P_N5AP
zBZ>*T(cm>LvVn>XdN%k9o;`cS2bh_`E%5ivhQHx&x4g1gZDi*vX9?81ZV#1DYJYDZ
zPs!p+Uhl>7Xs)O)Am5rJnl&U3Y
zN7W@>9HKP1)gCb0-$Mt`-aI$_A+{EE2lGTM6TT#5%@<-lLYKFMmW1z*
zXKpMbk<+-s)9AsPZDGTkV#Cv@4OV?bV^u0p)>VRj!w?iAAc;US2xih8gnE|c5K8-L
zqAu+v&)4d+b+4Jq>2GD&3rCm~*ZVZD*KJPpRNyO6l8Wz)gq;d%sS&slqCqf7IlhG0
zk__00LubwA(m2Dho(fVYPM%bVjuel%gY!zl@r}H`tz8Fj*dnv-r(hg4SfUan8#dto<28)TTIwJ|2}4Aq;~L9z|E&kwBuAN
zjSARlgguo+3QfNB);(Wq5;a!`;o^Oyf5lG|?nWy%2c3A2!D*k=&X4-Jjy}1=XY`XD
zF@^{=-h`Dj7}Qw6AY~hIWFrpIzy|AtE^NLTWz5&H&FRncTz^{BXD2H*Cwv+_?^CK3
z`_^&MN`VXb0kY=Ry?RZ@FLg7cZ4sN~^9uL-MTrb^n``=1Fcoe^+wTEa__5eKkmz#%
z#&0y{SLb5HE4o^a{r=NpN#c%sT*76tFU7uHHzMdSvO;8R1wreu*J=s+ad%8=Kv&|d
z0^?5XM2K`w6zdyLtsnx<6$&I$JOs)*6bg8PQeL7H%XbBpbbD
zqKKStyv(eV8Qm>tB-svrkAyv}x4uC9c7bZ#Xg0mD86HNbr-OT*w#bY6QU9bzb`eZ=
zx?L~qB8TtaKkOlMOCca61CdOsBkDmt=BuOhJ+h;F(cPR+OBw}!hoNENX?1633Ws1y
zu-Jkn`kSoN#dRxE&nLY>P%Nx-B)Ld;q+3Y*=n!$XCF3fbdkr_M)k3v!+SG#_?jGv;$B4d$;age^*7eCf{dcDhu~C|>V+FO0BsB4z8}82zpm}U`U8LH51*6T
zrCRavV*oq^#NUTIDA;Kei2p;bs^%Sgi_{-)PskcB-nZVV;pMra)z-9jF~`jXFB$RD
he;a!t^=|kMv&^pfYvz#DpM5A{NY`focy)m=_Ya3`k@o-q
literal 0
HcmV?d00001
diff --git a/tests/Images/Input/Bmp/pal4rletrns.bmp b/tests/Images/Input/Bmp/pal4rletrns.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..58994e92baf9812eb805c04807ca946b07f11185
GIT binary patch
literal 4326
zcmb_e%}*mo7JqcRtNjs(%MBPivtjH-+EtVTRs)hvOFPCsMYGaK$Tu!4A`Xbd&dDN*
zl=!rW6P6E=M{{sbkHm~Mhl#8lR?LB@ffdky$Nn4a?{&4mAem&fOHuCb>h7xd`*^Q<
z>#zS#Q~0-!-#?>&hyDb8hGL}Q_$}^RDb;6j&3||FeR$-v@br5e@lSVm>VV%nJaUI|
z458mqO6n!kCea!ElZ>+kX|%-AaICibH5q%$7Lkzg66u|jxlQfISt(mu%LqqmPv$o0
z@owHi%Z(aQ0<$J~*6tvjl|<{PMaI!^yjI`E%(QGSlAENTy^l*W
zTe6j&TcX9ImbB!&g||HYVNVita
zl4c8wI~lgciwjAqQn)rFO-RyP>C6(N(81j5jxdSmDLo$&RfMOG!zrI>OBNYWBxa
z7l7Ku%KnM5Nt^!Q?5xBi+*;h;96b-8sFwUE#s>Y#-`(4DOB1c_t}5&wSCf{fDYx*}
zZ>c+0me2ktbr+70|fN#)xIIHhdMDxRp-@WclF@oExh+iklizgOdBdsi{;r_V33iBvi2eX!RC$m-C|NMc1^w`4Us^yoCmyYk{
z=^t#Z101;mv}4D49e;363sOT(aC;_l!+rDSxjey{?yiE*FE*SFXK&xmb1M%UrkKpl
zMMzO9^*HCm@zF8ON@Sv>n7wQ6F$ywdAz|K
z$dsjOIvU@g6mrD%PDEZSt4^_UgngK+JPXQO9L%d3aCUrzilb(;OW|y|kJL5xa?C@_
z=GZ?GA_>Gsz(eJbFdOy+
z8@58NNzs46tdQYeq1;#~z>V{S4DdXVi%>Z>RvnzhrR%A)l=R@vzmFZM=1$aCxueOYh$Hpwppc9hwr#a~47)2wasN2tYM3`*D}xosSrub6V#%`}A2xX7Gu%W>@S=S>fb-oXQ!tD%?Py$bLV|2ykx7
zvRW04dUb#A3bZhfhO|#Oy18G{!5`_MG#m1dIyxtv{%JPHafekk;yijTCLOb)`Eajq
zcpNo?R$;G|BlM$s9Ayw^IPx@%7jcca9(pfr6I4?iPr4Z{k4wu3ni{pLq%$)!
zJWI%AlnGP(fx(bJWZRsyP$csn8GRC2ndLk(m@eMW=n?KlY
zY^gTBvb-L~_df?BGec!rt<`L==5_ts+rhT2ns57kzkkysGYkIPosR8wp#0U(Xc4;%%-{j0}}2bPqd7cr)i%;Bhfi%&g4U
z*XnCuRo*$lhlO<#{*6z@4gIB=ZDtR1-Ez15g6|!akslL%brPZXF@*VFM@UAoh^kzK
ziglXx>KDOna84FSvF($2Lq@ZS`%S%VbaB6X9bDl)stfN6dS^8eq8`?VU)L2GKgu2f
zQ~F*H;fo?wGAksd
z%sPqJ7XTbAG>l6Tq?-BUsD|VRZIsn`P?z@
zx#9tlBwyDR6S6@`#4_Rn(Inq?f02!H$c9xcZWUi`z1q6yC7+TWoW~moqH+0bZig0w
Xe>D%A-yn!@Ur_QT6@%bGM$h~Q4Wl~I
literal 0
HcmV?d00001
diff --git a/tests/Images/Input/Bmp/pal8oversizepal.bmp b/tests/Images/Input/Bmp/pal8oversizepal.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..93b8187ca1351aa20edad1a8c1da1c8a1bf74631
GIT binary patch
literal 9446
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
Nll3QGs>B9m{5Mp_Pk;ab
literal 0
HcmV?d00001
diff --git a/tests/Images/Input/Bmp/pal8rletrns.bmp b/tests/Images/Input/Bmp/pal8rletrns.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..a2af88d87cbb042f233e1ace72d69d5fc7b7a82f
GIT binary patch
literal 9212
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
literal 0
HcmV?d00001
diff --git a/tests/Images/Input/Bmp/rgb24largepal.bmp b/tests/Images/Input/Bmp/rgb24largepal.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..d5e418c2d41904a910afe101508025ab33915cdf
GIT binary patch
literal 25830
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