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"/> /// The <see cref="int"/>
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBitsNeededForColorDepth(int colors) public static int GetBitsNeededForColorDepth(int colors) => Math.Max(1, (int)Math.Ceiling(Math.Log(colors, 2)));
{
return 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> /// <summary>
/// Implementation of 1D Gaussian G(x) function /// Implementation of 1D Gaussian G(x) function
@ -132,10 +137,7 @@ namespace SixLabors.ImageSharp
/// The bounding <see cref="Rectangle"/>. /// The bounding <see cref="Rectangle"/>.
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rectangle GetBoundingRectangle(Point topLeft, Point bottomRight) public static Rectangle GetBoundingRectangle(Point topLeft, Point bottomRight) => new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y);
{
return new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y);
}
/// <summary> /// <summary>
/// Finds the bounding rectangle based on the first instance of any color component other /// 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 == 4
|| this.infoHeader.BitsPerPixel == 8) || this.infoHeader.BitsPerPixel == 8)
{ {
colorMapSize = (int)Math.Pow(2, this.infoHeader.BitsPerPixel) * 4; colorMapSize = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel) * 4;
} }
} }
else else

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

@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); 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; this.colorTableMode = this.colorTableMode ?? this.gifMetaData.ColorTableMode;
bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global); bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global);
@ -412,7 +412,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
int pixelCount = image.Palette.Length; int pixelCount = image.Palette.Length;
// The maximium number of colors for the bit depth // 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; Rgb24 rgb = default;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))

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

@ -9,6 +9,21 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
public enum PngBitDepth 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> /// <summary>
/// 8 bits per sample or per palette index (not per pixel). /// 8 bits per sample or per palette index (not per pixel).
/// </summary> /// </summary>

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

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

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

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

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

@ -48,11 +48,6 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
private readonly PngFilterMethod pngFilterMethod; private readonly PngFilterMethod pngFilterMethod;
/// <summary>
/// The quantizer for reducing the color count.
/// </summary>
private readonly IQuantizer quantizer;
/// <summary> /// <summary>
/// Gets or sets the CompressionLevel value /// Gets or sets the CompressionLevel value
/// </summary> /// </summary>
@ -63,6 +58,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
private readonly byte threshold; private readonly byte threshold;
/// <summary>
/// The quantizer for reducing the color count.
/// </summary>
private IQuantizer quantizer;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to write the gamma chunk /// Gets or sets a value indicating whether to write the gamma chunk
/// </summary> /// </summary>
@ -185,8 +185,6 @@ namespace SixLabors.ImageSharp.Formats.Png
this.gamma = this.gamma ?? pngMetaData.Gamma; this.gamma = this.gamma ?? pngMetaData.Gamma;
this.writeGamma = this.gamma > 0; this.writeGamma = this.gamma > 0;
this.pngColorType = this.pngColorType ?? pngMetaData.ColorType; this.pngColorType = this.pngColorType ?? pngMetaData.ColorType;
// TODO: We don't take full advantage of this information yet.
this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth; this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth;
this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16); this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16);
@ -196,17 +194,27 @@ namespace SixLabors.ImageSharp.Formats.Png
ReadOnlySpan<byte> quantizedPixelsSpan = default; ReadOnlySpan<byte> quantizedPixelsSpan = default;
if (this.pngColorType == PngColorType.Palette) 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. // Create quantized frame returning the palette and set the bit depth.
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame); quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
quantizedPixelsSpan = quantized.GetPixelSpan(); 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 // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
if (bits == 3) if (bits == 3)
{ {
bits = 4; bits = 4;
} }
else if (bits >= 5 || bits <= 7) else if (bits >= 5 && bits <= 7)
{ {
bits = 8; bits = 8;
} }
@ -556,7 +564,7 @@ namespace SixLabors.ImageSharp.Formats.Png
byte pixelCount = palette.Length.ToByte(); byte pixelCount = palette.Length.ToByte();
// Get max colors for bit depth. // 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; Rgba32 rgba = default;
bool anyAlpha = false; bool anyAlpha = false;
@ -700,7 +708,7 @@ namespace SixLabors.ImageSharp.Formats.Png
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, ReadOnlySpan<byte> quantizedPixelsSpan, Stream stream) private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, ReadOnlySpan<byte> quantizedPixelsSpan, Stream stream)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
this.bytesPerScanline = this.width * this.bytesPerPixel; this.bytesPerScanline = this.CalculateScanlineLength(this.width);
int resultLength = this.bytesPerScanline + 1; int resultLength = this.bytesPerScanline + 1;
this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean); 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 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] [Theory]
[WithTestPatternImages(nameof(BitsPerPixel), 24, 24, PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24)] [WithTestPatternImages(nameof(BitsPerPixel), 24, 24, PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24)]
public void Encode_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) 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? // The images are an exact match. Maybe the submodule isn't updating?
private const float ToleranceThresholdForPaletteEncoder = 1.3F / 100; 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> /// <summary>
/// All types except Palette /// All types except Palette
/// </summary> /// </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