From f0d8111d425f69f87aaf6f2e3d2f8f1a77b251be Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 18 Jun 2019 20:16:14 +0200 Subject: [PATCH 1/3] Add support for decoding RLE24 --- src/ImageSharp/Formats/Bmp/BmpCompression.cs | 17 +- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 175 +++++++++++++++++- src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 6 +- .../Formats/Bmp/BmpDecoderTests.cs | 16 ++ tests/ImageSharp.Tests/TestImages.cs | 3 + 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 8 files changed, 205 insertions(+), 12 deletions(-) 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 be275019e6..9c9162bd11 100644 --- a/src/ImageSharp/Formats/Bmp/BmpCompression.cs +++ b/src/ImageSharp/Formats/Bmp/BmpCompression.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. namespace SixLabors.ImageSharp.Formats.Bmp @@ -21,7 +21,7 @@ internal enum BmpCompression : int /// /// 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 @@ internal enum BmpCompression : int /// /// 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,13 @@ internal enum BmpCompression : int /// 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, + + /// + /// 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. + /// + RLE24 = 7, } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 1ceb35283a..b7ee2c1446 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -39,22 +39,22 @@ internal sealed class BmpDecoderCore 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; @@ -167,6 +167,12 @@ public Image Decode(Stream stream) } 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 +355,75 @@ private void ReadRle(BmpCompression compression, Buffer2D pixels } } + /// + /// 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 +620,95 @@ private void UncompressRle8(int w, Span buffer, Span undefinedPixels } /// - /// 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 +739,7 @@ private static void RleSkipEndOfBitmap( /// /// 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. diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index ca90020d85..4d7f781001 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -388,8 +388,12 @@ public static BmpInfoHeader ParseOs2Version2(ReadOnlySpan data) 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 a95703609b..f567f5355c 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -228,6 +228,22 @@ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit(TestImageProvider } } + [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) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 1019a5b084..6b88371792 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -230,6 +230,9 @@ public static class Bmp 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"; diff --git a/tests/Images/Input/Bmp/rgb24rle24.bmp b/tests/Images/Input/Bmp/rgb24rle24.bmp new file mode 100644 index 0000000000000000000000000000000000000000..360aee649c4e1b36d6dcc94d748a8b161cabdee5 GIT binary patch literal 21432 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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 From 5834c2021b2983429a65523f37764f41bd476b60 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 22 Jun 2019 11:52:54 +0200 Subject: [PATCH 2/3] Simplified determining colorMapSize, OS/2 always has 3 bytes for each palette entry --- src/ImageSharp/Formats/Bmp/BmpCompression.cs | 2 +- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 52 +++++++++++------- .../Formats/Bmp/BmpFileMarkerType.cs | 41 ++++++++++++++ .../Formats/Bmp/BmpDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Bmp/ba-bm.bmp | Bin 0 -> 9000 bytes 6 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 src/ImageSharp/Formats/Bmp/BmpFileMarkerType.cs create mode 100644 tests/Images/Input/Bmp/ba-bm.bmp diff --git a/src/ImageSharp/Formats/Bmp/BmpCompression.cs b/src/ImageSharp/Formats/Bmp/BmpCompression.cs index 9c9162bd11..c9adec4f00 100644 --- a/src/ImageSharp/Formats/Bmp/BmpCompression.cs +++ b/src/ImageSharp/Formats/Bmp/BmpCompression.cs @@ -4,7 +4,7 @@ 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 diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index b7ee2c1446..906fc53fef 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -78,6 +78,11 @@ internal sealed class BmpDecoderCore ///
private BmpFileHeader fileHeader; + /// + /// Indicates which bitmap file marker was read. + /// + private BmpFileMarkerType fileMarkerType; + /// /// The info header containing detailed information about the bitmap. /// @@ -1330,8 +1335,7 @@ private void ReadInfoHeader() /// /// 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]; @@ -1344,11 +1348,14 @@ private int ReadFileHeader() 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) @@ -1356,20 +1363,12 @@ private int ReadFileHeader() 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; } /// @@ -1381,7 +1380,7 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett { 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 @@ -1397,23 +1396,34 @@ private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palett } 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 0000000000..4abcaa3a08 --- /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/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index f567f5355c..dadce92d13 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -537,6 +537,7 @@ public void BmpDecoder_CanDecode_Os2v2Header(TestImageProvider p } [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 85efda0500..754ce20ca9 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -265,6 +265,7 @@ public static class Bmp 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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 Q2$1Z Date: Sat, 22 Jun 2019 11:57:25 +0200 Subject: [PATCH 3/3] Enum value for RLE24 is remapped to a different value, to be clearly separate from valid windows values. --- src/ImageSharp/Formats/Bmp/BmpCompression.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpCompression.cs b/src/ImageSharp/Formats/Bmp/BmpCompression.cs index c9adec4f00..27a0e121b6 100644 --- a/src/ImageSharp/Formats/Bmp/BmpCompression.cs +++ b/src/ImageSharp/Formats/Bmp/BmpCompression.cs @@ -63,10 +63,14 @@ internal enum BmpCompression : int 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 = 7, + RLE24 = 100, } }