Browse Source

Partial indexed png implementation.

Image renders but is skewed. Run ImageCanSaveIndexedPng test to see
progress.


Former-commit-id: f1d6faa94679bc7adb9cd20ea0b127504a61a33b
Former-commit-id: d5276cc71dad91c152082b5ab261157c9c0108e0
Former-commit-id: 1d5221d91b15cf6f0eb1f7988c32298d46b4e930
af/merge-core
James Jackson-South 10 years ago
parent
commit
f1d5048731
  1. 247
      src/ImageProcessorCore/Formats/Png/PngEncoder.cs
  2. 23
      tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs

247
src/ImageProcessorCore/Formats/Png/PngEncoder.cs

@ -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 = 65535;
/// <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; } = 128;
/// <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 <= 256
? (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 <= 256 ? 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[13];
WriteInteger(chunkData, 0, header.Width);
WriteInteger(chunkData, 4, header.Height);
chunkData[8] = header.BitDepth;
chunkData[9] = header.ColorType;
chunkData[10] = header.CompressionMethod;
chunkData[11] = header.FilterMethod;
chunkData[12] = 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 > 256)
{
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 <= 256)
{
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[13];
WriteInteger(chunkData, 0, header.Width);
WriteInteger(chunkData, 4, header.Height);
chunkData[8] = header.BitDepth;
chunkData[9] = header.ColorType;
chunkData[10] = header.CompressionMethod;
chunkData[11] = header.FilterMethod;
chunkData[12] = 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));
}
}
}

23
tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs

@ -68,6 +68,29 @@
}
}
[Fact]
public void ImageCanSaveIndexedPng()
{
if (!Directory.Exists("TestOutput/Indexed"))
{
Directory.CreateDirectory("TestOutput/Indexed");
}
foreach (string file in Files)
{
using (FileStream stream = File.OpenRead(file))
{
Image image = new Image(stream);
using (FileStream output = File.OpenWrite($"TestOutput/Indexed/{Path.GetFileNameWithoutExtension(file)}.png"))
{
image.Quality = 255;
image.Save(output, new PngFormat());
}
}
}
}
[Fact]
public void ImageCanConvertFormat()
{

Loading…
Cancel
Save