-
-
Notifications
You must be signed in to change notification settings - Fork 855
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
Adds support for encoding 8-bit bitmaps #906
Changes from 10 commits
3431ad3
7cfb560
6401344
6ce4efe
b5df1c7
4233bd2
e0afde9
e869573
9a6698f
7b8f9ef
b1d2947
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
using SixLabors.ImageSharp.Memory; | ||
using SixLabors.ImageSharp.Metadata; | ||
using SixLabors.ImageSharp.PixelFormats; | ||
using SixLabors.ImageSharp.Processing.Processors.Quantization; | ||
using SixLabors.Memory; | ||
|
||
namespace SixLabors.ImageSharp.Formats.Bmp | ||
|
@@ -43,6 +44,11 @@ internal sealed class BmpEncoderCore | |
/// </summary> | ||
private const int Rgba32BlueMask = 0xFF; | ||
|
||
/// <summary> | ||
/// The color palette for an 8 bit image will have 256 entry's with 4 bytes for each entry. | ||
/// </summary> | ||
private const int ColorPaletteSize8Bit = 1024; | ||
|
||
private readonly MemoryAllocator memoryAllocator; | ||
|
||
private Configuration configuration; | ||
|
@@ -142,11 +148,13 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream) | |
infoHeader.Compression = BmpCompression.BitFields; | ||
} | ||
|
||
int colorPaletteSize = this.bitsPerPixel == BmpBitsPerPixel.Pixel8 ? ColorPaletteSize8Bit : 0; | ||
|
||
var fileHeader = new BmpFileHeader( | ||
type: BmpConstants.TypeMarkers.Bitmap, | ||
fileSize: BmpFileHeader.Size + infoHeaderSize + infoHeader.ImageSize, | ||
reserved: 0, | ||
offset: BmpFileHeader.Size + infoHeaderSize); | ||
offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize); | ||
|
||
#if NETCOREAPP2_1 | ||
Span<byte> buffer = stackalloc byte[infoHeaderSize]; | ||
|
@@ -198,6 +206,10 @@ private void WriteImage<TPixel>(Stream stream, ImageFrame<TPixel> image) | |
case BmpBitsPerPixel.Pixel16: | ||
this.Write16Bit(stream, pixels); | ||
break; | ||
|
||
case BmpBitsPerPixel.Pixel8: | ||
this.Write8Bit(stream, image); | ||
break; | ||
} | ||
} | ||
|
||
|
@@ -276,5 +288,51 @@ private void Write16Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels) | |
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Writes an 8 Bit image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. | ||
/// </summary> | ||
/// <typeparam name="TPixel">The type of the pixel.</typeparam> | ||
/// <param name="stream">The <see cref="Stream"/> to write to.</param> | ||
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param> | ||
private void Write8Bit<TPixel>(Stream stream, ImageFrame<TPixel> image) | ||
where TPixel : struct, IPixel<TPixel> | ||
{ | ||
#if NETCOREAPP2_1 | ||
Span<byte> colorPalette = stackalloc byte[ColorPaletteSize8Bit]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1024 byte is too much for stackallock, we can remove this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the limit? I can never find an explicit instruction. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've read something in Joe Duffy's original material suggesting 128 bytes, based on their benchmarks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah ok. I use 257 bytes in the |
||
#else | ||
byte[] colorPalette = new byte[ColorPaletteSize8Bit]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't actually need the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean replacing GC array allocation. |
||
#endif | ||
|
||
var quantizer = new OctreeQuantizer(dither: true, maxColors: 256); | ||
QuantizedFrame<TPixel> quantized = quantizer.CreateFrameQuantizer<TPixel>(this.configuration).QuantizeFrame(image); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also: |
||
|
||
int idx = 0; | ||
var color = default(Rgba32); | ||
foreach (TPixel quantizedColor in quantized.Palette) | ||
{ | ||
quantizedColor.ToRgba32(ref color); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be a stupid idea and lack of understanding, but: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, in Octree we're working in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally forgot that it will lead to another allocation, so let's keep it for now. (An idea is starting to formulate in my mind, but of course it would break |
||
colorPalette[idx] = color.B; | ||
colorPalette[idx + 1] = color.G; | ||
colorPalette[idx + 2] = color.R; | ||
|
||
// Padding byte, always 0 | ||
colorPalette[idx + 3] = 0; | ||
idx += 4; | ||
} | ||
|
||
stream.Write(colorPalette, 0, ColorPaletteSize8Bit); | ||
|
||
for (int y = image.Height - 1; y >= 0; y--) | ||
{ | ||
Span<byte> pixelSpan = quantized.GetRowSpan(y); | ||
stream.Write(pixelSpan); | ||
|
||
for (int i = 0; i < this.padding; i++) | ||
{ | ||
stream.WriteByte(0); | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,10 +2,12 @@ | |
// Licensed under the Apache License, Version 2.0. | ||
|
||
using System.IO; | ||
|
||
using SixLabors.ImageSharp.Formats.Bmp; | ||
using SixLabors.ImageSharp.Metadata; | ||
using SixLabors.ImageSharp.PixelFormats; | ||
using SixLabors.ImageSharp.Processing; | ||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; | ||
|
||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
@@ -110,11 +112,7 @@ public void Encode_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> pro | |
[WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] | ||
[WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] | ||
[WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] | ||
// WinBmpv3 is a 24 bits per pixel image | ||
[WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] | ||
[WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] | ||
[WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] | ||
public void Encode_WithV3Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
public void Encode_32Bit_WithV3Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
// if supportTransparency is false, a v3 bitmap header will be written | ||
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); | ||
|
||
|
@@ -123,32 +121,92 @@ public void Encode_WithV3Header_Works<TPixel>(TestImageProvider<TPixel> provider | |
[WithFile(Bit32Rgba, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] | ||
[WithFile(WinBmpv4, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] | ||
[WithFile(WinBmpv5, PixelTypes.Rgba32 | PixelTypes.Rgb24, BmpBitsPerPixel.Pixel32)] | ||
public void Encode_32Bit_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); | ||
|
||
[Theory] | ||
// WinBmpv3 is a 24 bits per pixel image | ||
[WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] | ||
[WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] | ||
public void Encode_24Bit_WithV3Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); | ||
|
||
[Theory] | ||
[WithFile(WinBmpv3, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] | ||
[WithFile(F, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel24)] | ||
public void Encode_24Bit_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); | ||
|
||
|
||
[Theory] | ||
[WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] | ||
[WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] | ||
public void Encode_16Bit_WithV3Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); | ||
|
||
[Theory] | ||
[WithFile(Rgb16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] | ||
[WithFile(Bit16, PixelTypes.Bgra5551, BmpBitsPerPixel.Pixel16)] | ||
public void Encode_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
public void Encode_16Bit_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); | ||
|
||
[Theory] | ||
[WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] | ||
[WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] | ||
public void Encode_8Bit_WithV3Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: false); | ||
|
||
[Theory] | ||
[WithFile(WinBmpv5, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] | ||
[WithFile(Bit8Palette4, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel8)] | ||
public void Encode_8Bit_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); | ||
|
||
[Theory] | ||
[WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)] | ||
public void Encode_8BitGray_WithV3Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't say enough times, how much I ❤️ this kind of extensive and explanatory test coverage! This is an important enabler for us to do optimizations and refactors. |
||
where TPixel : struct, IPixel<TPixel> => | ||
TestBmpEncoderCore( | ||
provider, | ||
bitsPerPixel, | ||
supportTransparency: false, | ||
ImageComparer.TolerantPercentage(0.01f)); | ||
|
||
[Theory] | ||
[WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)] | ||
public void Encode_8BitGray_WithV4Header_Works<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
where TPixel : struct, IPixel<TPixel> => | ||
TestBmpEncoderCore( | ||
provider, | ||
bitsPerPixel, | ||
supportTransparency: true, | ||
ImageComparer.TolerantPercentage(0.01f)); | ||
|
||
[Theory] | ||
[WithFile(TestImages.Png.GrayAlpha2BitInterlaced, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] | ||
[WithFile(Bit32Rgba, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] | ||
public void Encode_PreservesAlpha<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) | ||
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); | ||
|
||
private static void TestBmpEncoderCore<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel, bool supportTransparency = true) | ||
private static void TestBmpEncoderCore<TPixel>( | ||
TestImageProvider<TPixel> provider, | ||
BmpBitsPerPixel bitsPerPixel, | ||
bool supportTransparency = true, | ||
ImageComparer customComparer = null) | ||
where TPixel : struct, IPixel<TPixel> | ||
{ | ||
using (Image<TPixel> image = provider.GetImage()) | ||
{ | ||
// There is no alpha in bmp with 24 bits per pixels, so the reference image will be made opaque. | ||
if (bitsPerPixel == BmpBitsPerPixel.Pixel24) | ||
// There is no alpha in bmp with less then 32 bits per pixels, so the reference image will be made opaque. | ||
if (bitsPerPixel != BmpBitsPerPixel.Pixel32) | ||
{ | ||
image.Mutate(c => c.MakeOpaque()); | ||
} | ||
|
||
var encoder = new BmpEncoder { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency }; | ||
|
||
// Does DebugSave & load reference CompareToReferenceInput(): | ||
image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder); | ||
image.VerifyEncoder(provider, "bmp", bitsPerPixel, encoder, customComparer); | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there other options?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, 4 bit and 1 bit