Browse Source

Png now correctly encodes 1, 2, 4 bit images

pull/693/head
James Jackson-South 8 years ago
parent
commit
ea0ab8bb50
  1. 18
      src/ImageSharp/Common/Helpers/ImageMaths.cs
  2. 2
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  3. 4
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 15
      src/ImageSharp/Formats/Png/PngBitDepth.cs
  5. 11
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  6. 3
      src/ImageSharp/Formats/Png/PngEncoder.cs
  7. 51
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  8. 1
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  9. 31
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

18
src/ImageSharp/Common/Helpers/ImageMaths.cs

@ -36,10 +36,15 @@ namespace SixLabors.ImageSharp
/// The <see cref="int"/>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBitsNeededForColorDepth(int colors)
{
return Math.Max(1, (int)Math.Ceiling(Math.Log(colors, 2)));
}
public static int GetBitsNeededForColorDepth(int colors) => Math.Max(1, (int)Math.Ceiling(Math.Log(colors, 2)));
/// <summary>
/// Returns how many colors will be created by the specified number of bits.
/// </summary>
/// <param name="bitDepth">The bit depth.</param>
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetColorCountForBitDepth(int bitDepth) => (int)Math.Pow(2, bitDepth);
/// <summary>
/// Implementation of 1D Gaussian G(x) function
@ -132,10 +137,7 @@ namespace SixLabors.ImageSharp
/// The bounding <see cref="Rectangle"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rectangle GetBoundingRectangle(Point topLeft, Point bottomRight)
{
return new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y);
}
public static Rectangle GetBoundingRectangle(Point topLeft, Point bottomRight) => new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y);
/// <summary>
/// Finds the bounding rectangle based on the first instance of any color component other

2
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -596,7 +596,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
|| this.infoHeader.BitsPerPixel == 4
|| this.infoHeader.BitsPerPixel == 8)
{
colorMapSize = (int)Math.Pow(2, this.infoHeader.BitsPerPixel) * 4;
colorMapSize = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel) * 4;
}
}
else

4
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
this.gifMetaData = image.MetaData.GetOrAddFormatMetaData<GifMetaData>(GifFormat.Instance);
this.gifMetaData = image.MetaData.GetOrAddFormatMetaData(GifFormat.Instance);
this.colorTableMode = this.colorTableMode ?? this.gifMetaData.ColorTableMode;
bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global);
@ -412,7 +412,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
int pixelCount = image.Palette.Length;
// The maximium number of colors for the bit depth
int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3;
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3;
Rgb24 rgb = default;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))

15
src/ImageSharp/Formats/Png/PngBitDepth.cs

@ -9,6 +9,21 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
public enum PngBitDepth
{
/// <summary>
/// 1 bit per sample or per palette index (not per pixel).
/// </summary>
Bit1 = 1,
/// <summary>
/// 2 bits per sample or per palette index (not per pixel).
/// </summary>
Bit2 = 2,
/// <summary>
/// 4 bits per sample or per palette index (not per pixel).
/// </summary>
Bit4 = 4,
/// <summary>
/// 8 bits per sample or per palette index (not per pixel).
/// </summary>

11
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -730,7 +730,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{
case PngColorType.Grayscale:
int factor = 255 / ((int)Math.Pow(2, this.header.BitDepth) - 1);
int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1);
if (!this.hasTrans)
{
@ -952,7 +952,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{
case PngColorType.Grayscale:
int factor = 255 / ((int)Math.Pow(2, this.header.BitDepth) - 1);
int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1);
if (!this.hasTrans)
{
@ -1303,12 +1303,7 @@ namespace SixLabors.ImageSharp.Formats.Png
filterMethod: data[11],
interlaceMethod: (PngInterlaceMode)data[12]);
// TODO: Figure out how we can determine the number of colors and support more bit depths.
if (bitDepth == 8 || bitDepth == 16)
{
pngMetaData.BitDepth = (PngBitDepth)bitDepth;
}
pngMetaData.BitDepth = (PngBitDepth)bitDepth;
pngMetaData.ColorType = this.header.ColorType;
}

3
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -4,7 +4,6 @@
using System.IO;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png
@ -45,7 +44,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets or sets quantizer for reducing the color count.
/// Defaults to the <see cref="WuQuantizer"/>
/// </summary>
public IQuantizer Quantizer { get; set; } = KnownQuantizers.Wu;
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.

51
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -48,11 +48,6 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
private readonly PngFilterMethod pngFilterMethod;
/// <summary>
/// The quantizer for reducing the color count.
/// </summary>
private readonly IQuantizer quantizer;
/// <summary>
/// Gets or sets the CompressionLevel value
/// </summary>
@ -63,6 +58,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
private readonly byte threshold;
/// <summary>
/// The quantizer for reducing the color count.
/// </summary>
private IQuantizer quantizer;
/// <summary>
/// Gets or sets a value indicating whether to write the gamma chunk
/// </summary>
@ -185,8 +185,6 @@ namespace SixLabors.ImageSharp.Formats.Png
this.gamma = this.gamma ?? pngMetaData.Gamma;
this.writeGamma = this.gamma > 0;
this.pngColorType = this.pngColorType ?? pngMetaData.ColorType;
// TODO: We don't take full advantage of this information yet.
this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth;
this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16);
@ -196,17 +194,27 @@ namespace SixLabors.ImageSharp.Formats.Png
ReadOnlySpan<byte> quantizedPixelsSpan = default;
if (this.pngColorType == PngColorType.Palette)
{
byte bits;
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
if (this.quantizer == null)
{
bits = (byte)Math.Min(8u, (short)this.pngBitDepth);
int colorSize = ImageMaths.GetColorCountForBitDepth(bits);
this.quantizer = new WuQuantizer(colorSize);
}
// Create quantized frame returning the palette and set the bit depth.
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
quantizedPixelsSpan = quantized.GetPixelSpan();
byte bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
if (bits == 3)
{
bits = 4;
}
else if (bits >= 5 || bits <= 7)
else if (bits >= 5 && bits <= 7)
{
bits = 8;
}
@ -556,7 +564,7 @@ namespace SixLabors.ImageSharp.Formats.Png
byte pixelCount = palette.Length.ToByte();
// Get max colors for bit depth.
int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3;
int colorTableLength = ImageMaths.GetColorCountForBitDepth(header.BitDepth) * 3;
Rgba32 rgba = default;
bool anyAlpha = false;
@ -700,7 +708,7 @@ namespace SixLabors.ImageSharp.Formats.Png
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, ReadOnlySpan<byte> quantizedPixelsSpan, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
this.bytesPerScanline = this.width * this.bytesPerPixel;
this.bytesPerScanline = this.CalculateScanlineLength(this.width);
int resultLength = this.bytesPerScanline + 1;
this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
@ -828,5 +836,26 @@ namespace SixLabors.ImageSharp.Formats.Png
stream.Write(this.buffer, 0, 4); // write the crc
}
/// <summary>
/// Calculates the scanline length.
/// </summary>
/// <param name="width">The width of the row.</param>
/// <returns>
/// The <see cref="int"/> representing the length.
/// </returns>
private int CalculateScanlineLength(int width)
{
int mod = this.bitDepth == 16 ? 16 : 8;
int scanlineLength = width * this.bitDepth * this.bytesPerPixel;
int amount = scanlineLength % mod;
if (amount != 0)
{
scanlineLength += mod - amount;
}
return scanlineLength / mod;
}
}
}

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

@ -88,7 +88,6 @@ namespace SixLabors.ImageSharp.Tests
}
}
[Theory]
[WithTestPatternImages(nameof(BitsPerPixel), 24, 24, PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24)]
public void Encode_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)

31
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -23,6 +23,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
// The images are an exact match. Maybe the submodule isn't updating?
private const float ToleranceThresholdForPaletteEncoder = 1.3F / 100;
public static readonly TheoryData<string, PngBitDepth> PngBitDepthFiles =
new TheoryData<string, PngBitDepth>
{
{ TestImages.Png.Rgb48Bpp, PngBitDepth.Bit16 },
{ TestImages.Png.Bpp1, PngBitDepth.Bit1 }
};
/// <summary>
/// All types except Palette
/// </summary>
@ -290,5 +297,29 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
}
[Theory]
[MemberData(nameof(PngBitDepthFiles))]
public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth)
{
var options = new PngEncoder();
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateImage())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
PngMetaData meta = output.MetaData.GetOrAddFormatMetaData(PngFormat.Instance);
Assert.Equal(pngBitDepth, meta.BitDepth);
}
}
}
}
}
}
Loading…
Cancel
Save