@ -9,6 +9,8 @@ namespace ImageProcessorCore.Formats
using System.IO ;
using System.Threading.Tasks ;
using ImageProcessorCore.Quantizers ;
/// <summary>
/// Image encoder for writing image data to a stream in png format.
/// </summary>
@ -19,11 +21,17 @@ namespace ImageProcessorCore.Formats
/// </summary>
private const int MaxBlockSize = 6 5 5 3 5 ;
/// <summary>
/// The number of bits required to encode the colors in the png.
/// </summary>
private byte bitDepth ;
private QuantizedImage quantized ;
/// <summary>
/// Gets or sets the quality of output for images.
/// </summary>
/// <remarks>Png is a lossless format so this is not used in this encoder.</remarks>
public int Quality { get ; set ; }
public int Quality { get ; set ; } = int . MaxValue ;
/// <inheritdoc/>
public string MimeType = > "image/png" ;
@ -51,6 +59,16 @@ namespace ImageProcessorCore.Formats
/// <value>The gamma value of the image.</value>
public double Gamma { get ; set ; } = 2.2F ;
/// <summary>
/// The quantizer for reducing the color count.
/// </summary>
public IQuantizer Quantizer { get ; set ; }
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
public byte Threshold { get ; set ; } = 1 2 8 ;
/// <inheritdoc/>
public bool IsSupportedFileExtension ( string extension )
{
@ -83,18 +101,35 @@ namespace ImageProcessorCore.Formats
0 ,
8 ) ;
this . Quality = image . Quality . Clamp ( 1 , int . MaxValue ) ;
this . bitDepth = this . Quality < = 2 5 6
? ( byte ) ( this . GetBitsNeededForColorDepth ( this . Quality ) . Clamp ( 1 , 8 ) )
: ( byte ) 8 ;
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
if ( this . bitDepth = = 3 )
{
this . bitDepth = 4 ;
}
else if ( this . bitDepth > = 5 | | this . bitDepth < = 7 )
{
this . bitDepth = 8 ;
}
PngHeader header = new PngHeader
{
Width = image . Width ,
Height = image . Height ,
ColorType = 6 , // Each pixel is an R,G,B triple, followed by an alpha sample.
BitDepth = 8 ,
ColorType = ( byte ) ( this . Quality < = 2 5 6 ? 3 : 6 ) , // 3 = indexed, 6= Each pixel is an R,G,B triple, followed by an alpha sample.
BitDepth = this . bitDepth ,
FilterMethod = 0 , // None
CompressionMethod = 0 ,
InterlaceMethod = 0
} ;
this . WriteHeaderChunk ( stream , header ) ;
this . WritePaletteChunk ( stream , header , image ) ;
this . WritePhysicalChunk ( stream , image ) ;
this . WriteGammaChunk ( stream ) ;
this . WriteDataChunks ( stream , image ) ;
@ -144,6 +179,79 @@ namespace ImageProcessorCore.Formats
stream . Write ( buffer , 0 , 4 ) ;
}
/// <summary>
/// Writes the header chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="header">The <see cref="PngHeader"/>.</param>
private void WriteHeaderChunk ( Stream stream , PngHeader header )
{
byte [ ] chunkData = new byte [ 1 3 ] ;
WriteInteger ( chunkData , 0 , header . Width ) ;
WriteInteger ( chunkData , 4 , header . Height ) ;
chunkData [ 8 ] = header . BitDepth ;
chunkData [ 9 ] = header . ColorType ;
chunkData [ 1 0 ] = header . CompressionMethod ;
chunkData [ 1 1 ] = header . FilterMethod ;
chunkData [ 1 2 ] = header . InterlaceMethod ;
this . WriteChunk ( stream , PngChunkTypes . Header , chunkData ) ;
}
/// <summary>
/// Writes the palette chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="header">The <see cref="PngHeader"/>.</param>
private void WritePaletteChunk ( Stream stream , PngHeader header , ImageBase image )
{
if ( this . Quality > 2 5 6 )
{
return ;
}
if ( this . Quantizer = = null )
{
this . Quantizer = new WuQuantizer { Threshold = this . Threshold } ;
}
// Quantize the image returning a palette.
this . quantized = this . Quantizer . Quantize ( image , this . Quality ) ;
// Grab the palette and write it to the stream.
Bgra32 [ ] palette = this . quantized . Palette ;
int pixelCount = palette . Length ;
// Get max colors for bit depth.
int colorTableLength = ( int ) Math . Pow ( 2 , header . BitDepth ) * 3 ;
byte [ ] colorTable = new byte [ colorTableLength ] ;
Parallel . For ( 0 , pixelCount ,
i = >
{
int offset = i * 3 ;
Bgra32 color = palette [ i ] ;
colorTable [ offset ] = color . R ;
colorTable [ offset + 1 ] = color . G ;
colorTable [ offset + 2 ] = color . B ;
} ) ;
this . WriteChunk ( stream , PngChunkTypes . Palette , colorTable ) ;
// Write the transparency data
if ( this . quantized . TransparentIndex > - 1 )
{
byte [ ] buffer = BitConverter . GetBytes ( this . quantized . TransparentIndex ) ;
Array . Reverse ( buffer ) ;
this . WriteChunk ( stream , PngChunkTypes . PaletteAlpha , buffer ) ;
}
}
/// <summary>
/// Writes the physical dimension information to the stream.
/// </summary>
@ -199,45 +307,86 @@ namespace ImageProcessorCore.Formats
/// <param name="image">The image base.</param>
private void WriteDataChunks ( Stream stream , ImageBase image )
{
byte [ ] data ;
int imageWidth = image . Width ;
int imageHeight = image . Height ;
byte [ ] data = new byte [ ( imageWidth * imageHeight * 4 ) + image . Height ] ;
int rowLength = ( imageWidth * 4 ) + 1 ;
Parallel . For ( 0 , imageHeight , y = >
// Indexed image.
if ( this . Quality < = 2 5 6 )
{
byte compression = 0 ;
if ( y > 0 )
{
compression = 2 ;
}
// TODO: I think I need to split then pad the beginning of each row.
// Split the array etc. Code below doesn't do this right.
// Time to read the spec... Again.
//data = new byte[(imageWidth * imageHeight) + image.Height];
//int rowLength = imageWidth;
//Parallel.For(0, imageHeight, y =>
//{
// byte compression = 0;
// if (y > 0)
// {
// compression = 2;
// }
// data[y * rowLength] = compression;
// for (int x = 0; x < imageWidth; x++)
// {
// // Calculate the offset for the new array.
// int dataOffset = (y * rowLength) + x + 1;
// data[dataOffset + 1] = this.quantized.Pixels[(y * rowLength) + x];
// if (y > 0)
// {
// data[dataOffset] -= this.quantized.Pixels[((y - 1) * rowLength) + x];
// }
// }
//});
// This outputs image but doesn't pad.
data = this . quantized . Pixels ;
}
else
{
// TrueColor image.
data = new byte [ ( imageWidth * imageHeight * 4 ) + image . Height ] ;
data [ y * rowLength ] = compression ;
int rowLength = ( imageWidth * 4 ) + 1 ;
for ( int x = 0 ; x < imageWidth ; x + + )
Parallel . For ( 0 , imageHeight , y = >
{
Bgra32 color = Color . ToNonPremultiplied ( image [ x , y ] ) ;
// Calculate the offset for the new array.
int dataOffset = ( y * rowLength ) + ( x * 4 ) + 1 ;
data [ dataOffset ] = color . R ;
data [ dataOffset + 1 ] = color . G ;
data [ dataOffset + 2 ] = color . B ;
data [ dataOffset + 3 ] = color . A ;
byte compression = 0 ;
if ( y > 0 )
{
color = Color . ToNonPremultiplied ( image [ x , y - 1 ] ) ;
compression = 2 ;
}
data [ y * rowLength ] = compression ;
data [ dataOffset ] - = color . R ;
data [ dataOffset + 1 ] - = color . G ;
data [ dataOffset + 2 ] - = color . B ;
data [ dataOffset + 3 ] - = color . A ;
for ( int x = 0 ; x < imageWidth ; x + + )
{
Bgra32 color = Color . ToNonPremultiplied ( image [ x , y ] ) ;
// Calculate the offset for the new array.
int dataOffset = ( y * rowLength ) + ( x * 4 ) + 1 ;
data [ dataOffset ] = color . R ;
data [ dataOffset + 1 ] = color . G ;
data [ dataOffset + 2 ] = color . B ;
data [ dataOffset + 3 ] = color . A ;
if ( y > 0 )
{
color = Color . ToNonPremultiplied ( image [ x , y - 1 ] ) ;
data [ dataOffset ] - = color . R ;
data [ dataOffset + 1 ] - = color . G ;
data [ dataOffset + 2 ] - = color . B ;
data [ dataOffset + 3 ] - = color . A ;
}
}
}
} ) ;
} ) ;
}
byte [ ] buffer ;
int bufferLength ;
@ -289,27 +438,6 @@ namespace ImageProcessorCore.Formats
this . WriteChunk ( stream , PngChunkTypes . End , null ) ;
}
/// <summary>
/// Writes the header chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="header">The <see cref="PngHeader"/>.</param>
private void WriteHeaderChunk ( Stream stream , PngHeader header )
{
byte [ ] chunkData = new byte [ 1 3 ] ;
WriteInteger ( chunkData , 0 , header . Width ) ;
WriteInteger ( chunkData , 4 , header . Height ) ;
chunkData [ 8 ] = header . BitDepth ;
chunkData [ 9 ] = header . ColorType ;
chunkData [ 1 0 ] = header . CompressionMethod ;
chunkData [ 1 1 ] = header . FilterMethod ;
chunkData [ 1 2 ] = header . InterlaceMethod ;
this . WriteChunk ( stream , PngChunkTypes . Header , chunkData ) ;
}
/// <summary>
/// Writes a chunk to the stream.
/// </summary>
@ -356,5 +484,18 @@ namespace ImageProcessorCore.Formats
WriteInteger ( stream , ( uint ) crc32 . Value ) ;
}
/// <summary>
/// Returns how many bits are required to store the specified number of colors.
/// Performs a Log2() on the value.
/// </summary>
/// <param name="colors">The number of colors.</param>
/// <returns>
/// The <see cref="int"/>
/// </returns>
private int GetBitsNeededForColorDepth ( int colors )
{
return ( int ) Math . Ceiling ( Math . Log ( colors , 2 ) ) ;
}
}
}