Skip to content

Commit 2b5a575

Browse files
authored
Merge pull request #909 from brianpopow/feature/dontUseQuantizeOnGray8
Not using quantization for Grey8 images when encoding 8Bit Bitmaps
2 parents ef736e0 + bb8911d commit 2b5a575

File tree

4 files changed

+135
-21
lines changed

4 files changed

+135
-21
lines changed

src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Buffers;
66
using System.IO;
7+
using System.Runtime.InteropServices;
78

89
using SixLabors.ImageSharp.Advanced;
910
using SixLabors.ImageSharp.Common.Helpers;
@@ -305,23 +306,46 @@ private void Write16Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
305306
private void Write8Bit<TPixel>(Stream stream, ImageFrame<TPixel> image)
306307
where TPixel : struct, IPixel<TPixel>
307308
{
309+
bool isGray8 = typeof(TPixel) == typeof(Gray8);
308310
using (IMemoryOwner<byte> colorPaletteBuffer = this.memoryAllocator.AllocateManagedByteBuffer(ColorPaletteSize8Bit, AllocationOptions.Clean))
309-
using (IQuantizedFrame<TPixel> quantized = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, 256).QuantizeFrame(image))
310311
{
311312
Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
312-
int idx = 0;
313+
if (isGray8)
314+
{
315+
this.Write8BitGray(stream, image, colorPalette);
316+
}
317+
else
318+
{
319+
this.Write8BitColor(stream, image, colorPalette);
320+
}
321+
}
322+
}
323+
324+
/// <summary>
325+
/// Writes an 8 Bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
326+
/// </summary>
327+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
328+
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
329+
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
330+
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
331+
private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
332+
where TPixel : struct, IPixel<TPixel>
333+
{
334+
using (IQuantizedFrame<TPixel> quantized = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, 256).QuantizeFrame(image))
335+
{
336+
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
313337
var color = default(Rgba32);
314-
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
315338

316339
// TODO: Use bulk conversion here for better perf
317-
foreach (TPixel quantizedColor in paletteSpan)
340+
int idx = 0;
341+
foreach (TPixel quantizedColor in quantizedColors)
318342
{
319343
quantizedColor.ToRgba32(ref color);
320344
colorPalette[idx] = color.B;
321345
colorPalette[idx + 1] = color.G;
322346
colorPalette[idx + 2] = color.R;
323347

324-
// Padding byte, always 0
348+
// Padding byte, always 0.
325349
colorPalette[idx + 3] = 0;
326350
idx += 4;
327351
}
@@ -340,5 +364,43 @@ private void Write8Bit<TPixel>(Stream stream, ImageFrame<TPixel> image)
340364
}
341365
}
342366
}
367+
368+
/// <summary>
369+
/// Writes an 8 Bit gray image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
370+
/// </summary>
371+
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
372+
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
373+
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
374+
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
375+
private void Write8BitGray<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
376+
where TPixel : struct, IPixel<TPixel>
377+
{
378+
// Create a color palette with 256 different gray values.
379+
for (int i = 0; i <= 255; i++)
380+
{
381+
int idx = i * 4;
382+
byte grayValue = (byte)i;
383+
colorPalette[idx] = grayValue;
384+
colorPalette[idx + 1] = grayValue;
385+
colorPalette[idx + 2] = grayValue;
386+
387+
// Padding byte, always 0.
388+
colorPalette[idx + 3] = 0;
389+
}
390+
391+
stream.Write(colorPalette);
392+
393+
for (int y = image.Height - 1; y >= 0; y--)
394+
{
395+
ReadOnlySpan<TPixel> inputPixelRow = image.GetPixelRowSpan(y);
396+
ReadOnlySpan<byte> outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow);
397+
stream.Write(outputPixelRow);
398+
399+
for (int i = 0; i < this.padding; i++)
400+
{
401+
stream.WriteByte(0);
402+
}
403+
}
404+
}
343405
}
344406
}

tests/ImageSharp.Tests/Drawing/DrawImageTests.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class DrawImageTests
3232
};
3333

3434
[Theory]
35-
[WithFile( TestImages.Png.Rainbow,nameof(BlendingModes), PixelTypes.Rgba32)]
35+
[WithFile(TestImages.Png.Rainbow, nameof(BlendingModes), PixelTypes.Rgba32)]
3636
public void ImageBlendingMatchesSvgSpecExamples<TPixel>(TestImageProvider<TPixel> provider, PixelColorBlendingMode mode)
3737
where TPixel : struct, IPixel<TPixel>
3838
{
@@ -54,13 +54,13 @@ public void ImageBlendingMatchesSvgSpecExamples<TPixel>(TestImageProvider<TPixel
5454
appendSourceFileOrDescription: false);
5555
}
5656
}
57-
57+
5858
[Theory]
5959
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, TestImages.Png.Splash, PixelColorBlendingMode.Normal, 1f)]
6060
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Bgr24, TestImages.Png.Bike, PixelColorBlendingMode.Normal, 1f)]
6161
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, TestImages.Png.Splash, PixelColorBlendingMode.Normal, 0.75f)]
6262
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, TestImages.Png.Splash, PixelColorBlendingMode.Normal, 0.25f)]
63-
63+
6464
[WithTestPatternImages(400, 400, PixelTypes.Rgba32, TestImages.Png.Splash, PixelColorBlendingMode.Multiply, 0.5f)]
6565
[WithTestPatternImages(400, 400, PixelTypes.Rgba32, TestImages.Png.Splash, PixelColorBlendingMode.Add, 0.5f)]
6666
[WithTestPatternImages(400, 400, PixelTypes.Rgba32, TestImages.Png.Splash, PixelColorBlendingMode.Subtract, 0.5f)]
@@ -77,7 +77,7 @@ public void WorksWithDifferentConfigurations<TPixel>(
7777
using (Image<TPixel> image = provider.GetImage())
7878
using (var blend = Image.Load<TPixel>(TestFile.Create(brushImage).Bytes))
7979
{
80-
Size size = new Size(image.Width * 3 / 4, image.Height *3/ 4);
80+
Size size = new Size(image.Width * 3 / 4, image.Height * 3 / 4);
8181
Point position = new Point(image.Width / 8, image.Height / 8);
8282
blend.Mutate(x => x.Resize(size.Width, size.Height, KnownResamplers.Bicubic));
8383
image.Mutate(x => x.DrawImage(blend, position, mode, opacity));
@@ -89,7 +89,7 @@ public void WorksWithDifferentConfigurations<TPixel>(
8989
{
9090
encoder.BitDepth = PngBitDepth.Bit16;
9191
}
92-
92+
9393
image.DebugSave(provider, testInfo, encoder: encoder);
9494
image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.01f),
9595
provider,
@@ -138,15 +138,15 @@ public void WorksWithDifferentLocations(TestImageProvider<Rgba32> provider, int
138138
testOutputDetails: $"{x}_{y}",
139139
appendPixelTypeToFileName: false,
140140
appendSourceFileOrDescription: false);
141-
141+
142142
background.CompareToReferenceOutput(
143143
provider,
144144
testOutputDetails: $"{x}_{y}",
145145
appendPixelTypeToFileName: false,
146146
appendSourceFileOrDescription: false);
147147
}
148148
}
149-
149+
150150
[Theory]
151151
[WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)]
152152
public void DrawTransformed<TPixel>(TestImageProvider<TPixel> provider)
@@ -166,12 +166,12 @@ public void DrawTransformed<TPixel>(TestImageProvider<TPixel> provider)
166166

167167
// Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor
168168
var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2);
169-
image.Mutate(x => x.DrawImage(blend, position, .75F));
170-
169+
image.Mutate(x => x.DrawImage(blend, position, .75F));
170+
171171
image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false);
172172
image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.002f),
173173
provider,
174-
appendSourceFileOrDescription: false,
174+
appendSourceFileOrDescription: false,
175175
appendPixelTypeToFileName: false);
176176
}
177177
}
@@ -197,6 +197,6 @@ void Test()
197197
}
198198
}
199199

200-
200+
201201
}
202202
}

tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
using System.IO;
55

6+
using SixLabors.ImageSharp.Formats;
67
using SixLabors.ImageSharp.Formats.Bmp;
78
using SixLabors.ImageSharp.Metadata;
89
using SixLabors.ImageSharp.PixelFormats;
910
using SixLabors.ImageSharp.Processing;
11+
using SixLabors.ImageSharp.Processing.Processors.Quantization;
1012
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
1113

1214
using Xunit;
@@ -169,8 +171,7 @@ public void Encode_8BitGray_WithV3Header_Works<TPixel>(TestImageProvider<TPixel>
169171
TestBmpEncoderCore(
170172
provider,
171173
bitsPerPixel,
172-
supportTransparency: false,
173-
ImageComparer.TolerantPercentage(0.01f));
174+
supportTransparency: false);
174175

175176
[Theory]
176177
[WithFile(Bit8Gs, PixelTypes.Gray8, BmpBitsPerPixel.Pixel8)]
@@ -179,8 +180,59 @@ public void Encode_8BitGray_WithV4Header_Works<TPixel>(TestImageProvider<TPixel>
179180
TestBmpEncoderCore(
180181
provider,
181182
bitsPerPixel,
182-
supportTransparency: true,
183-
ImageComparer.TolerantPercentage(0.01f));
183+
supportTransparency: true);
184+
185+
[Theory]
186+
[WithFile(Bit32Rgb, PixelTypes.Rgba32)]
187+
public void Encode_8BitColor_WithWuQuantizer<TPixel>(TestImageProvider<TPixel> provider)
188+
where TPixel : struct, IPixel<TPixel>
189+
{
190+
if (!TestEnvironment.Is64BitProcess)
191+
{
192+
return;
193+
}
194+
195+
using (Image<TPixel> image = provider.GetImage())
196+
{
197+
var encoder = new BmpEncoder
198+
{
199+
BitsPerPixel = BmpBitsPerPixel.Pixel8,
200+
Quantizer = new WuQuantizer(256)
201+
};
202+
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false);
203+
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);
204+
using (var referenceImage = Image.Load<TPixel>(actualOutputFile, referenceDecoder))
205+
{
206+
referenceImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.01f), provider, extension: "bmp", appendPixelTypeToFileName: false);
207+
}
208+
}
209+
}
210+
211+
[Theory]
212+
[WithFile(Bit32Rgb, PixelTypes.Rgba32)]
213+
public void Encode_8BitColor_WithOctreeQuantizer<TPixel>(TestImageProvider<TPixel> provider)
214+
where TPixel : struct, IPixel<TPixel>
215+
{
216+
if (!TestEnvironment.Is64BitProcess)
217+
{
218+
return;
219+
}
220+
221+
using (Image<TPixel> image = provider.GetImage())
222+
{
223+
var encoder = new BmpEncoder
224+
{
225+
BitsPerPixel = BmpBitsPerPixel.Pixel8,
226+
Quantizer = new OctreeQuantizer(256)
227+
};
228+
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false);
229+
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);
230+
using (var referenceImage = Image.Load<TPixel>(actualOutputFile, referenceDecoder))
231+
{
232+
referenceImage.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.01f), provider, extension: "bmp", appendPixelTypeToFileName: false);
233+
}
234+
}
235+
}
184236

185237
[Theory]
186238
[WithFile(TestImages.Png.GrayAlpha2BitInterlaced, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)]

0 commit comments

Comments
 (0)