From f9f7a564b18b07f91d970933b23945cd7404cbd2 Mon Sep 17 00:00:00 2001
From: Brian Popow <38701097+brianpopow@users.noreply.github.com>
Date: Mon, 1 Jul 2019 07:12:48 +0200
Subject: [PATCH] Add support for decoding RLE24 Bitmaps (#939)
* Add support for decoding RLE24
* Simplified determining colorMapSize, OS/2 always has 3 bytes for each palette entry
* Enum value for RLE24 is remapped to a different value, to be clearly separate from valid windows values.
---
src/ImageSharp/Formats/Bmp/BmpCompression.cs | 23 +-
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 227 +++++++++++++++---
.../Formats/Bmp/BmpFileMarkerType.cs | 41 ++++
src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 6 +-
.../Formats/Bmp/BmpDecoderTests.cs | 17 ++
tests/ImageSharp.Tests/TestImages.cs | 4 +
tests/Images/Input/Bmp/ba-bm.bmp | Bin 0 -> 9000 bytes
tests/Images/Input/Bmp/rgb24rle24.bmp | Bin 0 -> 21432 bytes
tests/Images/Input/Bmp/rle24rlecut.bmp | Bin 0 -> 16748 bytes
tests/Images/Input/Bmp/rle24rletrns.bmp | Bin 0 -> 20036 bytes
10 files changed, 284 insertions(+), 34 deletions(-)
create mode 100644 src/ImageSharp/Formats/Bmp/BmpFileMarkerType.cs
create mode 100644 tests/Images/Input/Bmp/ba-bm.bmp
create mode 100644 tests/Images/Input/Bmp/rgb24rle24.bmp
create mode 100644 tests/Images/Input/Bmp/rle24rlecut.bmp
create mode 100644 tests/Images/Input/Bmp/rle24rletrns.bmp
diff --git a/src/ImageSharp/Formats/Bmp/BmpCompression.cs b/src/ImageSharp/Formats/Bmp/BmpCompression.cs
index be275019e..27a0e121b 100644
--- a/src/ImageSharp/Formats/Bmp/BmpCompression.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpCompression.cs
@@ -1,10 +1,10 @@
-// Copyright (c) Six Labors and contributors.
+// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Bmp
{
///
- /// Defines how the compression type of the image data
+ /// Defines the compression type of the image data
/// in the bitmap file.
///
internal enum BmpCompression : int
@@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Two bytes are one data record. If the first byte is not zero, the
- /// next two half bytes will be repeated as much as the value of the first byte.
+ /// next byte will be repeated as much as the value of the first byte.
/// If the first byte is zero, the record has different meanings, depending
/// on the second byte. If the second byte is zero, it is the end of the row,
/// if it is one, it is the end of the image.
@@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Two bytes are one data record. If the first byte is not zero, the
- /// next byte will be repeated as much as the value of the first byte.
+ /// next two half bytes will be repeated as much as the value of the first byte.
/// If the first byte is zero, the record has different meanings, depending
/// on the second byte. If the second byte is zero, it is the end of the row,
/// if it is one, it is the end of the image.
@@ -60,6 +60,17 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// Specifies that the bitmap is not compressed and that the color table consists of four DWORD color
/// masks that specify the red, green, blue, and alpha components of each pixel.
///
- BI_ALPHABITFIELDS = 6
+ BI_ALPHABITFIELDS = 6,
+
+ ///
+ /// OS/2 specific compression type.
+ /// Similar to run length encoding of 4 and 8 bit.
+ /// The only difference is that run values encoded are three bytes in size (one byte per RGB color component),
+ /// rather than four or eight bits in size.
+ ///
+ /// Note: Because compression value of 4 is ambiguous for BI_RGB for windows and RLE24 for OS/2, the enum value is remapped
+ /// to a different value.
+ ///
+ RLE24 = 100,
}
-}
\ No newline at end of file
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 1ceb35283..906fc53fe 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -39,22 +39,22 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private const int DefaultRgb16BMask = 0x1F;
///
- /// RLE8 flag value that indicates following byte has special meaning.
+ /// RLE flag value that indicates following byte has special meaning.
///
private const int RleCommand = 0x00;
///
- /// RLE8 flag value marking end of a scan line.
+ /// RLE flag value marking end of a scan line.
///
private const int RleEndOfLine = 0x00;
///
- /// RLE8 flag value marking end of bitmap data.
+ /// RLE flag value marking end of bitmap data.
///
private const int RleEndOfBitmap = 0x01;
///
- /// RLE8 flag value marking the start of [x,y] offset instruction.
+ /// RLE flag value marking the start of [x,y] offset instruction.
///
private const int RleDelta = 0x02;
@@ -78,6 +78,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
private BmpFileHeader fileHeader;
+ ///
+ /// Indicates which bitmap file marker was read.
+ ///
+ private BmpFileMarkerType fileMarkerType;
+
///
/// The info header containing detailed information about the bitmap.
///
@@ -167,6 +172,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
break;
+
+ case BmpCompression.RLE24:
+ this.ReadRle24(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
+
+ break;
+
case BmpCompression.RLE8:
case BmpCompression.RLE4:
this.ReadRle(this.infoHeader.Compression, pixels, palette, this.infoHeader.Width, this.infoHeader.Height, inverted);
@@ -349,6 +360,75 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
}
+ ///
+ /// Looks up color values and builds the image from de-compressed RLE24.
+ ///
+ /// The pixel format.
+ /// The to assign the palette to.
+ /// The width of the bitmap.
+ /// The height of the bitmap.
+ /// Whether the bitmap is inverted.
+ private void ReadRle24(Buffer2D pixels, int width, int height, bool inverted)
+ where TPixel : struct, IPixel
+ {
+ TPixel color = default;
+ using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 3, 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;
+ Span bufferSpan = buffer.GetSpan();
+ this.UncompressRle24(width, bufferSpan, undefinedPixels.GetSpan(), rowsWithUndefinedPixelsSpan);
+ for (int y = 0; y < height; y++)
+ {
+ int newY = Invert(y, height, inverted);
+ Span pixelRow = pixels.GetRowSpan(newY);
+ bool rowHasUndefinedPixels = rowsWithUndefinedPixelsSpan[y];
+ if (rowHasUndefinedPixels)
+ {
+ // Slow path with undefined pixels.
+ for (int x = 0; x < width; x++)
+ {
+ int idx = (y * width * 3) + (x * 3);
+ if (undefinedPixels[x, y])
+ {
+ switch (this.options.RleSkippedPixelHandling)
+ {
+ case RleSkippedPixelHandling.FirstColorOfPalette:
+ color.FromBgr24(Unsafe.As(ref bufferSpan[idx]));
+ 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 bufferSpan[idx]));
+ }
+
+ pixelRow[x] = color;
+ }
+ }
+ else
+ {
+ // Fast path without any undefined pixels.
+ for (int x = 0; x < width; x++)
+ {
+ int idx = (y * width * 3) + (x * 3);
+ color.FromBgr24(Unsafe.As(ref bufferSpan[idx]));
+ pixelRow[x] = color;
+ }
+ }
+ }
+ }
+ }
+
///
/// Produce uncompressed bitmap data from a RLE4 stream.
///
@@ -545,7 +625,95 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
///
- /// Keeps track of skipped / undefined pixels, when EndOfBitmap command occurs.
+ /// Produce uncompressed bitmap data from a RLE24 stream.
+ ///
+ ///
+ ///
If first byte is 0, the second byte may have special meaning.
+ ///
Otherwise, the first byte is the length of the run and following three bytes are the color for the run.
+ ///
+ /// The width of the bitmap.
+ /// Buffer for uncompressed data.
+ /// Keeps track of skipped and therefore undefined pixels.
+ /// Keeps track of rows, which have undefined pixels.
+ private void UncompressRle24(int w, Span buffer, Span undefinedPixels, Span rowsWithUndefinedPixels)
+ {
+#if NETCOREAPP2_1
+ Span cmd = stackalloc byte[2];
+#else
+ byte[] cmd = new byte[2];
+#endif
+ int uncompressedPixels = 0;
+
+ while (uncompressedPixels < buffer.Length)
+ {
+ if (this.stream.Read(cmd, 0, cmd.Length) != 2)
+ {
+ BmpThrowHelper.ThrowImageFormatException("Failed to read 2 bytes from stream while uncompressing RLE24 bitmap.");
+ }
+
+ if (cmd[0] == RleCommand)
+ {
+ switch (cmd[1])
+ {
+ case RleEndOfBitmap:
+ int skipEoB = (buffer.Length - (uncompressedPixels * 3)) / 3;
+ RleSkipEndOfBitmap(uncompressedPixels, w, skipEoB, undefinedPixels, rowsWithUndefinedPixels);
+
+ return;
+
+ case RleEndOfLine:
+ uncompressedPixels += RleSkipEndOfLine(uncompressedPixels, w, undefinedPixels, rowsWithUndefinedPixels);
+
+ break;
+
+ case RleDelta:
+ int dx = this.stream.ReadByte();
+ int dy = this.stream.ReadByte();
+ uncompressedPixels += RleSkipDelta(uncompressedPixels, 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.
+ int length = cmd[1];
+
+ byte[] run = new byte[length * 3];
+
+ this.stream.Read(run, 0, run.Length);
+
+ run.AsSpan().CopyTo(buffer.Slice(start: uncompressedPixels * 3));
+
+ uncompressedPixels += length;
+
+ // Absolute mode data is aligned to two-byte word-boundary.
+ int padding = run.Length & 1;
+
+ this.stream.Skip(padding);
+
+ break;
+ }
+ }
+ else
+ {
+ int max = uncompressedPixels + cmd[0];
+ byte blueIdx = cmd[1];
+ byte greenIdx = (byte)this.stream.ReadByte();
+ byte redIdx = (byte)this.stream.ReadByte();
+
+ int bufferIdx = uncompressedPixels * 3;
+ for (; uncompressedPixels < max; uncompressedPixels++)
+ {
+ buffer[bufferIdx++] = blueIdx;
+ buffer[bufferIdx++] = greenIdx;
+ buffer[bufferIdx++] = redIdx;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Keeps track of skipped / undefined pixels, when the EndOfBitmap command occurs.
///
/// The already processed pixel count.
/// The width of the image.
@@ -576,7 +744,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Keeps track of undefined / skipped pixels, when the EndOfLine command occurs.
///
- /// The already processed pixel count.
+ /// The already uncompressed pixel count.
/// The width of image.
/// The undefined pixels.
/// The rows with undefined pixels.
@@ -1167,8 +1335,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
///
/// Reads the from the stream.
///
- /// The color map size in bytes, if it could be determined by the file header. Otherwise -1.
- private int ReadFileHeader()
+ private void ReadFileHeader()
{
#if NETCOREAPP2_1
Span buffer = stackalloc byte[BmpFileHeader.Size];
@@ -1181,11 +1348,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp
switch (fileTypeMarker)
{
case BmpConstants.TypeMarkers.Bitmap:
+ this.fileMarkerType = BmpFileMarkerType.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.fileMarkerType = BmpFileMarkerType.BitmapArray;
+
+ // Because we only decode the first bitmap in the array, the array header will be ignored.
+ // The bitmap file header of the first image follows the array header.
this.stream.Read(buffer, 0, BmpFileHeader.Size);
this.fileHeader = BmpFileHeader.Parse(buffer);
if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
@@ -1193,20 +1363,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
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;
}
///
@@ -1218,7 +1380,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
{
this.stream = stream;
- int colorMapSizeBytes = this.ReadFileHeader();
+ this.ReadFileHeader();
this.ReadInfoHeader();
// see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517
@@ -1234,23 +1396,34 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
int bytesPerColorMapEntry = 4;
-
+ int colorMapSizeBytes = -1;
if (this.infoHeader.ClrUsed == 0)
{
if (this.infoHeader.BitsPerPixel == 1
|| this.infoHeader.BitsPerPixel == 4
|| this.infoHeader.BitsPerPixel == 8)
{
- if (colorMapSizeBytes == -1)
+ switch (this.fileMarkerType)
{
- colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
- }
+ case BmpFileMarkerType.Bitmap:
+ colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
+ int colorCountForBitDepth = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel);
+ bytesPerColorMapEntry = colorMapSizeBytes / colorCountForBitDepth;
- 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);
- // Edge case for less-than-full-sized palette: bytesPerColorMapEntry should be at least 3.
- bytesPerColorMapEntry = Math.Max(bytesPerColorMapEntry, 3);
+ break;
+ case BmpFileMarkerType.BitmapArray:
+ case BmpFileMarkerType.ColorIcon:
+ case BmpFileMarkerType.ColorPointer:
+ case BmpFileMarkerType.Icon:
+ case BmpFileMarkerType.Pointer:
+ // OS/2 bitmaps always have 3 colors per color palette entry.
+ bytesPerColorMapEntry = 3;
+ colorMapSizeBytes = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel) * bytesPerColorMapEntry;
+ break;
+ }
}
}
else
diff --git a/src/ImageSharp/Formats/Bmp/BmpFileMarkerType.cs b/src/ImageSharp/Formats/Bmp/BmpFileMarkerType.cs
new file mode 100644
index 000000000..4abcaa3a0
--- /dev/null
+++ b/src/ImageSharp/Formats/Bmp/BmpFileMarkerType.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.Bmp
+{
+ ///
+ /// Indicates which bitmap file marker was read.
+ ///
+ public enum BmpFileMarkerType
+ {
+ ///
+ /// Single-image BMP file that may have been created under Windows or OS/2.
+ ///
+ Bitmap,
+
+ ///
+ /// OS/2 Bitmap Array.
+ ///
+ BitmapArray,
+
+ ///
+ /// OS/2 Color Icon.
+ ///
+ ColorIcon,
+
+ ///
+ /// OS/2 Color Pointer.
+ ///
+ ColorPointer,
+
+ ///
+ /// OS/2 Icon.
+ ///
+ Icon,
+
+ ///
+ /// OS/2 Pointer.
+ ///
+ Pointer
+ }
+}
diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
index ca90020d8..4d7f78100 100644
--- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
@@ -388,8 +388,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
case 2:
infoHeader.Compression = BmpCompression.RLE4;
break;
+ case 4:
+ infoHeader.Compression = BmpCompression.RLE24;
+ break;
default:
- BmpThrowHelper.ThrowImageFormatException($"Compression type is not supported. ImageSharp only supports uncompressed, RLE4 and RLE8.");
+ // Compression type 3 (1DHuffman) is not supported.
+ BmpThrowHelper.ThrowImageFormatException("Compression type is not supported. ImageSharp only supports uncompressed, RLE4, RLE8 and RLE24.");
break;
}
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index a95703609..dadce92d1 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
@@ -228,6 +228,22 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
}
}
+ [Theory]
+ [WithFile(RLE24, PixelTypes.Rgba32)]
+ [WithFile(RLE24Cut, PixelTypes.Rgba32)]
+ [WithFile(RLE24Delta, PixelTypes.Rgba32)]
+ public void BmpDecoder_CanDecode_RunLengthEncoded_24Bit(TestImageProvider provider)
+ where TPixel : struct, IPixel
+ {
+ using (Image image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.Black }))
+ {
+ image.DebugSave(provider);
+
+ // TODO: Neither System.Drawing nor MagickReferenceDecoder decode this file.
+ // image.CompareToOriginal(provider);
+ }
+ }
+
[Theory]
[WithFile(RgbaAlphaBitfields, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeAlphaBitfields(TestImageProvider provider)
@@ -521,6 +537,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
}
[Theory]
+ [WithFile(Os2BitmapArray, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArray9s, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArrayDiamond, PixelTypes.Rgba32)]
[WithFile(Os2BitmapArraySkater, PixelTypes.Rgba32)]
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 3c732c32d..754ce20ca 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -231,6 +231,9 @@ namespace SixLabors.ImageSharp.Tests
public const string NegHeight = "Bmp/neg_height.bmp";
public const string CoreHeader = "Bmp/BitmapCoreHeaderQR.bmp";
public const string V5Header = "Bmp/BITMAPV5HEADER.bmp";
+ public const string RLE24 = "Bmp/rgb24rle24.bmp";
+ public const string RLE24Cut = "Bmp/rle24rlecut.bmp";
+ public const string RLE24Delta = "Bmp/rle24rlecut.bmp";
public const string RLE8 = "Bmp/RunLengthEncoded.bmp";
public const string RLE8Cut = "Bmp/pal8rlecut.bmp";
public const string RLE8Delta = "Bmp/pal8rletrns.bmp";
@@ -262,6 +265,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Bit8Palette4 = "Bmp/pal8-0.bmp";
public const string Os2v2Short = "Bmp/pal8os2v2-16.bmp";
public const string Os2v2 = "Bmp/pal8os2v2.bmp";
+ public const string Os2BitmapArray = "Bmp/ba-bm.bmp";
public const string Os2BitmapArray9s = "Bmp/9S.BMP";
public const string Os2BitmapArrayDiamond = "Bmp/DIAMOND.BMP";
public const string Os2BitmapArrayMarble = "Bmp/GMARBLE.BMP";
diff --git a/tests/Images/Input/Bmp/ba-bm.bmp b/tests/Images/Input/Bmp/ba-bm.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..d2615bde3e6dd7d1b43e715183bf0fbdb0245058
GIT binary patch
literal 9000
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
J>WhJ={|AKWt3m((
literal 0
HcmV?d00001
diff --git a/tests/Images/Input/Bmp/rle24rletrns.bmp b/tests/Images/Input/Bmp/rle24rletrns.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..baa86167fa9c2c6940b84b7565b59cb7991dcc74
GIT binary patch
literal 20036
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
literal 0
HcmV?d00001