From 50848a6d276f39afe2f99f50bc2f8adcdcd74d36 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 | 3 + tests/Images/Input/Bmp/rgb24rle24.bmp | 3 + tests/Images/Input/Bmp/rle24rlecut.bmp | 3 + tests/Images/Input/Bmp/rle24rletrns.bmp | 3 + 10 files changed, 296 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 000000000..a787229ac --- /dev/null +++ b/tests/Images/Input/Bmp/ba-bm.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ec70510334952d3fbeae51a9a49d4e50e5afc292a1f9232970a7cf22b1a18fc +size 9000 diff --git a/tests/Images/Input/Bmp/rgb24rle24.bmp b/tests/Images/Input/Bmp/rgb24rle24.bmp new file mode 100644 index 000000000..0e0731dd5 --- /dev/null +++ b/tests/Images/Input/Bmp/rgb24rle24.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:351d358824671a79dc63147a78fc555d46cbee357661674e80c898e133e0b5c5 +size 21432 diff --git a/tests/Images/Input/Bmp/rle24rlecut.bmp b/tests/Images/Input/Bmp/rle24rlecut.bmp new file mode 100644 index 000000000..137d38647 --- /dev/null +++ b/tests/Images/Input/Bmp/rle24rlecut.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15b84ee3e41934653939197267758e6719da93d017200a7b9e61820b368af04c +size 16748 diff --git a/tests/Images/Input/Bmp/rle24rletrns.bmp b/tests/Images/Input/Bmp/rle24rletrns.bmp new file mode 100644 index 000000000..bc5dc14a9 --- /dev/null +++ b/tests/Images/Input/Bmp/rle24rletrns.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37caf0742ebc94e4ff73b822052091db543559fa96352b83a3e5f5545999c5f7 +size 20036