diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs
index 3c48488ec..cacaca0bb 100644
--- a/src/ImageSharp/Common/Helpers/ImageMaths.cs
+++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs
@@ -36,10 +36,15 @@ namespace SixLabors.ImageSharp
/// The
///
[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)));
+
+ ///
+ /// Returns how many colors will be created by the specified number of bits.
+ ///
+ /// The bit depth.
+ /// The
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetColorCountForBitDepth(int bitDepth) => (int)Math.Pow(2, bitDepth);
///
/// Implementation of 1D Gaussian G(x) function
@@ -132,10 +137,7 @@ namespace SixLabors.ImageSharp
/// The bounding .
///
[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);
///
/// Finds the bounding rectangle based on the first instance of any color component other
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 3bb44f1d0..a574d5178 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/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
diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
index 7a880b0f9..f2e0eab5c 100644
--- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs
+++ b/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(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))
diff --git a/src/ImageSharp/Formats/Png/PngBitDepth.cs b/src/ImageSharp/Formats/Png/PngBitDepth.cs
index 0c22a4c91..396f2c160 100644
--- a/src/ImageSharp/Formats/Png/PngBitDepth.cs
+++ b/src/ImageSharp/Formats/Png/PngBitDepth.cs
@@ -9,6 +9,21 @@ namespace SixLabors.ImageSharp.Formats.Png
///
public enum PngBitDepth
{
+ ///
+ /// 1 bit per sample or per palette index (not per pixel).
+ ///
+ Bit1 = 1,
+
+ ///
+ /// 2 bits per sample or per palette index (not per pixel).
+ ///
+ Bit2 = 2,
+
+ ///
+ /// 4 bits per sample or per palette index (not per pixel).
+ ///
+ Bit4 = 4,
+
///
/// 8 bits per sample or per palette index (not per pixel).
///
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 4bc483301..3daee991c 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/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;
}
diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs
index 435d0abbc..05d687a88 100644
--- a/src/ImageSharp/Formats/Png/PngEncoder.cs
+++ b/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
///
- public IQuantizer Quantizer { get; set; } = KnownQuantizers.Wu;
+ public IQuantizer Quantizer { get; set; }
///
/// Gets or sets the transparency threshold.
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index 9d9de71b1..20fc8b8e3 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -48,11 +48,6 @@ namespace SixLabors.ImageSharp.Formats.Png
///
private readonly PngFilterMethod pngFilterMethod;
- ///
- /// The quantizer for reducing the color count.
- ///
- private readonly IQuantizer quantizer;
-
///
/// Gets or sets the CompressionLevel value
///
@@ -63,6 +58,11 @@ namespace SixLabors.ImageSharp.Formats.Png
///
private readonly byte threshold;
+ ///
+ /// The quantizer for reducing the color count.
+ ///
+ private IQuantizer quantizer;
+
///
/// Gets or sets a value indicating whether to write the gamma chunk
///
@@ -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 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().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(ImageFrame pixels, ReadOnlySpan quantizedPixelsSpan, Stream stream)
where TPixel : struct, IPixel
{
- 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
}
+
+ ///
+ /// Calculates the scanline length.
+ ///
+ /// The width of the row.
+ ///
+ /// The representing the length.
+ ///
+ 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;
+ }
}
}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
index c75c65691..311b28f2d 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
+++ b/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(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel)
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index 62de45064..c9435a37d 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/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 PngBitDepthFiles =
+ new TheoryData
+ {
+ { TestImages.Png.Rgb48Bpp, PngBitDepth.Bit16 },
+ { TestImages.Png.Bpp1, PngBitDepth.Bit1 }
+ };
+
///
/// All types except Palette
///
@@ -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 input = testFile.CreateImage())
+ {
+ using (var memStream = new MemoryStream())
+ {
+ input.Save(memStream, options);
+
+ memStream.Position = 0;
+ using (var output = Image.Load(memStream))
+ {
+ PngMetaData meta = output.MetaData.GetOrAddFormatMetaData(PngFormat.Instance);
+
+ Assert.Equal(pngBitDepth, meta.BitDepth);
+ }
+ }
+ }
+ }
}
}
\ No newline at end of file