Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Bitmap RLE undefined pixel handling #927

Merged
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/ImageSharp/Formats/Bmp/BmpDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <list type="bullet">
/// <item>JPG</item>
/// <item>PNG</item>
/// <item>RLE4</item>
/// <item>Some OS/2 specific subtypes like: Bitmap Array, Color Icon, Color Pointer, Icon, Pointer.</item>
/// </list>
/// Formats will be supported in a later releases. We advise always
/// to use only 24 Bit Windows bitmaps.
/// </remarks>
public sealed class BmpDecoder : IImageDecoder, IBmpDecoderOptions, IImageInfoDetector
{
/// <summary>
/// Gets or sets a value indicating how to deal with skipped pixels, which can occur during decoding run length encoded bitmaps.
/// </summary>
public RleSkippedPixelHandling RleSkippedPixelHandling { get; set; } = RleSkippedPixelHandling.Black;

/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : struct, IPixel<TPixel>
Expand Down
139 changes: 106 additions & 33 deletions src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,19 @@ internal sealed class BmpEncoderCore
/// </summary>
private const int ColorPaletteSize8Bit = 1024;

/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;

/// <summary>
/// The global configuration.
/// </summary>
private Configuration configuration;

/// <summary>
/// The color depth, in number of bits per pixel.
/// </summary>
private BmpBitsPerPixel? bitsPerPixel;

/// <summary>
Expand Down
6 changes: 2 additions & 4 deletions src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.

using System;
using System.Buffers.Binary;

namespace SixLabors.ImageSharp.Formats.Bmp
{
Expand All @@ -21,10 +22,7 @@ public IImageFormat DetectFormat(ReadOnlySpan<byte> header)

private bool IsSupportedFileFormat(ReadOnlySpan<byte> 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;
}
}
}
5 changes: 4 additions & 1 deletion src/ImageSharp/Formats/Bmp/IBmpDecoderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
internal interface IBmpDecoderOptions
{
// added this for consistency so we can add stuff as required, no options currently available
/// <summary>
/// Gets the value indicating how to deal with skipped pixels, which can occur during decoding run length encoded bitmaps.
/// </summary>
RleSkippedPixelHandling RleSkippedPixelHandling { get; }
}
}
26 changes: 26 additions & 0 deletions src/ImageSharp/Formats/Bmp/RleSkippedPixelHandling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

namespace SixLabors.ImageSharp.Formats.Bmp
{
/// <summary>
/// Defines possible options, how skipped pixels during decoding of run length encoded bitmaps should be treated.
/// </summary>
public enum RleSkippedPixelHandling : int
{
/// <summary>
/// Undefined pixels should be black. This is how System.Drawing handles undefined pixels.
/// </summary>
Black = 0,

/// <summary>
/// Undefined pixels should be transparent.
/// </summary>
Transparent = 1,

/// <summary>
/// Undefined pixels should have the first color of the palette.
/// </summary>
FirstColorOfPalette = 2
}
}
2 changes: 1 addition & 1 deletion tests/ImageSharp.Tests/FileTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public abstract class FileTestBase
/// <summary>
/// A collection of all the bmp test images
/// </summary>
public static IEnumerable<string> AllBmpFiles = TestImages.Bmp.All;
public static IEnumerable<string> AllBmpFiles = TestImages.Bmp.Benchmark;

/// <summary>
/// A collection of all the jpeg test images
Expand Down
210 changes: 200 additions & 10 deletions tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,27 +21,26 @@ public class BmpDecoderTests
{
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<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
{
{ 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<TPixel>(TestImageProvider<TPixel> provider)
[WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_MiscellaneousBitmaps<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
{
image.DebugSave(provider);

if (TestEnvironment.IsWindows)
{
image.CompareToOriginal(provider);
Expand All @@ -60,6 +60,158 @@ public void BmpDecoder_CanDecodeBitfields<TPixel>(TestImageProvider<TPixel> prov
}
}

[Theory]
[WithFile(Bit16Inverted, PixelTypes.Rgba32)]
[WithFile(Bit8Inverted, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_Inverted<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
{
image.DebugSave(provider);
image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder());
JimBobSquarePants marked this conversation as resolved.
Show resolved Hide resolved
}
}

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

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

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

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

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

[Theory]
[WithFile(RLE4cut, PixelTypes.Rgba32)]
[WithFile(RLE4delta, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.Black }))
{
image.DebugSave(provider);
if (TestEnvironment.IsWindows)
{
image.CompareToOriginal(provider);
}
}
}

[Theory]
[WithFile(RLE4, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_RunLengthEncoded_4Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.Black }))
{
image.DebugSave(provider);
if (TestEnvironment.IsWindows)
{
image.CompareToOriginal(provider);
}
}
}

[Theory]
[WithFile(RLE8cut, PixelTypes.Rgba32)]
[WithFile(RLE8delta, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.Black }))
{
image.DebugSave(provider);
if (TestEnvironment.IsWindows)
{
image.CompareToOriginal(provider);
}
}
}

[Theory]
[WithFile(RLE8, PixelTypes.Rgba32)]
[WithFile(RLE8Inverted, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder() { RleSkippedPixelHandling = RleSkippedPixelHandling.Black }))
{
image.DebugSave(provider);
if (TestEnvironment.IsWindows)
{
image.CompareToOriginal(provider);
}
}
}

[Theory]
[WithFile(RgbaAlphaBitfields, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeAlphaBitfields<TPixel>(TestImageProvider<TPixel> provider)
Expand Down Expand Up @@ -106,6 +258,7 @@ public void BmpDecoder_CanDecodeBitfields_WithUnusualBitmasks<TPixel>(TestImageP

[Theory]
[WithFile(WinBmpv2, PixelTypes.Rgba32)]
[WithFile(CoreHeader, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeBmpv2<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Expand Down Expand Up @@ -141,7 +294,41 @@ public void BmpDecoder_CanDecodeLessThanFullPalette<TPixel>(TestImageProvider<TP
}

[Theory]
[WithFile(Rgba32bf56, PixelTypes.Rgba32)]
[WithFile(OversizedPalette, PixelTypes.Rgba32)]
[WithFile(Rgb24LargePalette, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeOversizedPalette<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
{
image.DebugSave(provider);
if (TestEnvironment.IsWindows)
{
image.CompareToOriginal(provider);
}
}
}

[Theory]
[WithFile(InvalidPaletteSize, PixelTypes.Rgba32)]
public void BmpDecoder_ThrowsImageFormatException_OnInvalidPaletteSize<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Assert.Throws<ImageFormatException>( () => { using (Image<TPixel> image = provider.GetImage(new BmpDecoder())) { } });
}

[Theory]
[WithFile(Rgb24jpeg, PixelTypes.Rgba32)]
[WithFile(Rgb24png, PixelTypes.Rgba32)]
public void BmpDecoder_ThrowsNotSupportedException_OnUnsupportedBitmaps<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Assert.Throws<NotSupportedException>(() => { using (Image<TPixel> image = provider.GetImage(new BmpDecoder())) { } });
}

[Theory]
[WithFile(Rgba32bf56AdobeV3, PixelTypes.Rgba32)]
[WithFile(Rgb32h52AdobeV3, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeAdobeBmpv3<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Expand All @@ -166,6 +353,7 @@ public void BmpDecoder_CanDecodeBmpv4<TPixel>(TestImageProvider<TPixel> provider

[Theory]
[WithFile(WinBmpv5, PixelTypes.Rgba32)]
[WithFile(V5Header, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeBmpv5<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
Expand Down Expand Up @@ -223,6 +411,8 @@ public void BmpDecoder_CanDecode4BytePerEntryPalette<TPixel>(TestImageProvider<T
[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);
Expand Down Expand Up @@ -281,4 +471,4 @@ public void BmpDecoder_CanDecode_Os2v2Header<TPixel>(TestImageProvider<TPixel> p
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public void EncodeJpeg(int executionCount, int quality, JpegSubsample subsample)
return;
}

string[] testFiles = TestImages.Bmp.All
string[] testFiles = TestImages.Bmp.Benchmark
.Concat(new[] { TestImages.Jpeg.Baseline.Calliphora, TestImages.Jpeg.Baseline.Cmyk }).ToArray();

Image<Rgba32>[] testImages = testFiles.Select(
Expand Down
Loading