diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
index 796a13a5e7..3b8aea6695 100644
--- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
+++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
@@ -11,14 +11,20 @@ namespace SixLabors.ImageSharp.Formats.Png
internal interface IPngEncoderOptions
{
///
- /// Gets the png color type
+ /// Gets the number of bits per sample or per palette index (not per pixel).
+ /// Not all values are allowed for all values.
///
- PngColorType PngColorType { get; }
+ PngBitDepth BitDepth { get; }
///
- /// Gets the png filter method.
+ /// Gets the color type
///
- PngFilterMethod PngFilterMethod { get; }
+ PngColorType ColorType { get; }
+
+ ///
+ /// Gets the filter method.
+ ///
+ PngFilterMethod FilterMethod { get; }
///
/// Gets the compression level 1-9.
diff --git a/src/ImageSharp/Formats/Png/PngBitDepth.cs b/src/ImageSharp/Formats/Png/PngBitDepth.cs
new file mode 100644
index 0000000000..0c22a4c913
--- /dev/null
+++ b/src/ImageSharp/Formats/Png/PngBitDepth.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+// Note the value assignment, This will allow us to add 1, 2, and 4 bit encoding when we support it.
+namespace SixLabors.ImageSharp.Formats.Png
+{
+ ///
+ /// Provides enumeration for the available PNG bit depths.
+ ///
+ public enum PngBitDepth
+ {
+ ///
+ /// 8 bits per sample or per palette index (not per pixel).
+ ///
+ Bit8 = 8,
+
+ ///
+ /// 16 bits per sample or per palette index (not per pixel).
+ ///
+ Bit16 = 16
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index e4e583d194..48eb54768b 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -680,6 +680,8 @@ namespace SixLabors.ImageSharp.Formats.Png
case PngColorType.Grayscale:
int factor = 255 / ((int)Math.Pow(2, this.header.BitDepth) - 1);
+
+ // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
ReadOnlySpan scanline = ToArrayByBitsLength(scanlineBuffer, this.bytesPerScanline, this.header.BitDepth);
if (!this.hasTrans)
@@ -896,6 +898,8 @@ namespace SixLabors.ImageSharp.Formats.Png
case PngColorType.Grayscale:
int factor = 255 / ((int)Math.Pow(2, this.header.BitDepth) - 1);
+
+ // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
ReadOnlySpan scanline = ToArrayByBitsLength(scanlineBuffer, this.bytesPerScanline, this.header.BitDepth);
if (!this.hasTrans)
diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs
index fab1b51850..babda2effc 100644
--- a/src/ImageSharp/Formats/Png/PngEncoder.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoder.cs
@@ -14,14 +14,20 @@ namespace SixLabors.ImageSharp.Formats.Png
public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions
{
///
- /// Gets or sets the png color type.
+ /// Gets or sets the number of bits per sample or per palette index (not per pixel).
+ /// Not all values are allowed for all values.
///
- public PngColorType PngColorType { get; set; } = PngColorType.RgbWithAlpha;
+ public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8;
///
- /// Gets or sets the png filter method.
+ /// Gets or sets the color type.
///
- public PngFilterMethod PngFilterMethod { get; set; } = PngFilterMethod.Adaptive;
+ public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha;
+
+ ///
+ /// Gets or sets the filter method.
+ ///
+ public PngFilterMethod FilterMethod { get; set; } = PngFilterMethod.Paeth;
///
/// Gets or sets the compression level 1-9.
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index bfa20fb5ec..2c516b8293 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -5,6 +5,8 @@ using System;
using System.Buffers.Binary;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Png.Zlib;
@@ -41,6 +43,16 @@ namespace SixLabors.ImageSharp.Formats.Png
///
private readonly Crc32 crc = new Crc32();
+ ///
+ /// The png bit depth
+ ///
+ private readonly PngBitDepth pngBitDepth;
+
+ ///
+ /// Gets or sets a value indicating whether to use 16 bit encoding for supported color types.
+ ///
+ private readonly bool use16Bit;
+
///
/// The png color type.
///
@@ -149,8 +161,10 @@ namespace SixLabors.ImageSharp.Formats.Png
public PngEncoderCore(MemoryAllocator memoryAllocator, IPngEncoderOptions options)
{
this.memoryAllocator = memoryAllocator;
- this.pngColorType = options.PngColorType;
- this.pngFilterMethod = options.PngFilterMethod;
+ this.pngBitDepth = options.BitDepth;
+ this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16);
+ this.pngColorType = options.ColorType;
+ this.pngFilterMethod = options.FilterMethod;
this.compressionLevel = options.CompressionLevel;
this.gamma = options.Gamma;
this.quantizer = options.Quantizer;
@@ -197,8 +211,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
else
{
- // TODO: How do we set this in the options while keeping the value inline with the PngColorType?
- this.bitDepth = 8;
+ this.bitDepth = (byte)(this.use16Bit ? 16 : 8);
}
this.bytesPerPixel = this.CalculateBytesPerPixel();
@@ -206,10 +219,10 @@ namespace SixLabors.ImageSharp.Formats.Png
var header = new PngHeader(
width: image.Width,
height: image.Height,
- colorType: this.pngColorType,
bitDepth: this.bitDepth,
- filterMethod: 0, // None
- compressionMethod: 0,
+ colorType: this.pngColorType,
+ compressionMethod: 0, // None
+ filterMethod: 0,
interlaceMethod: 0); // TODO: Can't write interlaced yet.
this.WriteHeaderChunk(stream, header);
@@ -247,28 +260,62 @@ namespace SixLabors.ImageSharp.Formats.Png
private void CollectGrayscaleBytes(ReadOnlySpan rowSpan)
where TPixel : struct, IPixel
{
- byte[] rawScanlineArray = this.rawScanline.Array;
- var rgba = default(Rgba32);
+ // Use ITU-R recommendation 709 to match libpng.
+ const float RX = .2126F;
+ const float GX = .7152F;
+ const float BX = .0722F;
+ Span rawScanlineSpan = this.rawScanline.GetSpan();
- // Copy the pixels across from the image.
- // Reuse the chunk type buffer.
- for (int x = 0; x < this.width; x++)
+ if (this.pngColorType.Equals(PngColorType.Grayscale))
{
- // Convert the color to YCbCr and store the luminance
- // Optionally store the original color alpha.
- int offset = x * this.bytesPerPixel;
- rowSpan[x].ToRgba32(ref rgba);
- byte luminance = (byte)((0.299F * rgba.R) + (0.587F * rgba.G) + (0.114F * rgba.B));
-
- for (int i = 0; i < this.bytesPerPixel; i++)
+ // TODO: Realistically we should support 1, 2, 4, 8, and 16 bit grayscale images.
+ // we currently do the other types via palette. Maybe RC as I don't understand how the data is packed yet
+ // for 1, 2, and 4 bit grayscale images.
+ if (this.use16Bit)
{
- if (i == 0)
+ // 16 bit grayscale
+ Rgb48 rgb = default;
+ for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 2)
{
- rawScanlineArray[offset] = luminance;
+ rowSpan[x].ToRgb48(ref rgb);
+ ushort luminance = (ushort)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B));
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance);
}
- else
+ }
+ else
+ {
+ // 8 bit grayscale
+ Rgb24 rgb = default;
+ for (int x = 0; x < rowSpan.Length; x++)
{
- rawScanlineArray[offset + i] = rgba.A;
+ rowSpan[x].ToRgb24(ref rgb);
+ rawScanlineSpan[x] = (byte)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B));
+ }
+ }
+ }
+ else
+ {
+ if (this.use16Bit)
+ {
+ // 16 bit grayscale + alpha
+ Rgba64 rgba = default;
+ for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 4)
+ {
+ rowSpan[x].ToRgba64(ref rgba);
+ ushort luminance = (ushort)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B));
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance);
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.A);
+ }
+ }
+ else
+ {
+ // 8 bit grayscale + alpha
+ Rgba32 rgba = default;
+ for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 2)
+ {
+ rowSpan[x].ToRgba32(ref rgba);
+ rawScanlineSpan[o] = (byte)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B));
+ rawScanlineSpan[o + 1] = rgba.A;
}
}
}
@@ -282,14 +329,54 @@ namespace SixLabors.ImageSharp.Formats.Png
private void CollectTPixelBytes(ReadOnlySpan rowSpan)
where TPixel : struct, IPixel
{
- // TODO: We need to cater for 64bit mode here.
- if (this.bytesPerPixel == 4)
- {
- PixelOperations.Instance.ToRgba32Bytes(rowSpan, this.rawScanline.GetSpan(), this.width);
- }
- else
+ Span rawScanlineSpan = this.rawScanline.GetSpan();
+
+ switch (this.bytesPerPixel)
{
- PixelOperations.Instance.ToRgb24Bytes(rowSpan, this.rawScanline.GetSpan(), this.width);
+ case 4:
+ {
+ // 8 bit Rgba
+ PixelOperations.Instance.ToRgba32Bytes(rowSpan, rawScanlineSpan, this.width);
+ break;
+ }
+
+ case 3:
+ {
+ // 8 bit Rgb
+ PixelOperations.Instance.ToRgb24Bytes(rowSpan, rawScanlineSpan, this.width);
+ break;
+ }
+
+ case 8:
+ {
+ // 16 bit Rgba
+ Rgba64 rgba = default;
+ for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 8)
+ {
+ rowSpan[x].ToRgba64(ref rgba);
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgba.R);
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.G);
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgba.B);
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 6, 2), rgba.A);
+ }
+
+ break;
+ }
+
+ default:
+ {
+ // 16 bit Rgb
+ Rgb48 rgb = default;
+ for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 6)
+ {
+ rowSpan[x].ToRgb48(ref rgb);
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgb.R);
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgb.G);
+ BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgb.B);
+ }
+
+ break;
+ }
}
}
@@ -367,6 +454,9 @@ namespace SixLabors.ImageSharp.Formats.Png
// early on which shaves a couple of milliseconds off the processing time.
UpFilter.Encode(scanSpan, prevSpan, this.up.GetSpan(), out int currentSum);
+ // TODO: PERF.. We should be breaking out of the encoding for each line as soon as we hit the sum.
+ // That way the above comment would actually be true. It used to be anyway...
+ // If we could use SIMD for none branching filters we could really speed it up.
int lowestSum = currentSum;
IManagedByteBuffer actualResult = this.up;
@@ -402,26 +492,23 @@ namespace SixLabors.ImageSharp.Formats.Png
/// The
private int CalculateBytesPerPixel()
{
- // TODO: Cater for 64 bit here and below
switch (this.pngColorType)
{
case PngColorType.Grayscale:
- return 1;
+ return this.use16Bit ? 2 : 1;
case PngColorType.GrayscaleWithAlpha:
- return 2;
+ return this.use16Bit ? 4 : 2;
case PngColorType.Palette:
return 1;
case PngColorType.Rgb:
- return 3;
+ return this.use16Bit ? 6 : 3;
// PngColorType.RgbWithAlpha
- // TODO: Maybe figure out a way to detect if there are any transparent
- // pixels and encode RGB if none.
default:
- return 4;
+ return this.use16Bit ? 8 : 4;
}
}
diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
index 084b93b398..97b498ee4e 100644
--- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
+++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
@@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Tests
using (Image image = provider.GetImage())
{
image.Mutate(c => c.Quantize(quantizer));
- image.DebugSave(provider, new PngEncoder() { PngColorType = PngColorType.Palette }, testOutputDetails: quantizerName);
+ image.DebugSave(provider, new PngEncoder() { ColorType = PngColorType.Palette }, testOutputDetails: quantizerName);
}
provider.Configuration.MemoryAllocator.ReleaseRetainedResources();
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index 11124ad030..eb046165d5 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -130,8 +130,8 @@ namespace SixLabors.ImageSharp.Tests
var encoder = new PngEncoder
{
- PngColorType = pngColorType,
- PngFilterMethod = pngFilterMethod,
+ ColorType = pngColorType,
+ FilterMethod = pngFilterMethod,
CompressionLevel = compressionLevel,
Quantizer = new WuQuantizer(paletteSize)
};
diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceCodecTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceCodecTests.cs
index ee398c87b7..520b8d93fb 100644
--- a/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceCodecTests.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/Tests/ReferenceCodecTests.cs
@@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Tests
sourceImage.Mutate(c => c.MakeOpaque());
}
- var encoder = new PngEncoder() { PngColorType = pngColorType };
+ var encoder = new PngEncoder() { ColorType = pngColorType };
return provider.Utility.SaveTestOutputFile(sourceImage, "png", encoder);
}
}