|
|
|
@ -6,6 +6,7 @@ |
|
|
|
namespace ImageProcessorCore.Formats |
|
|
|
{ |
|
|
|
using System; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.IO; |
|
|
|
using System.Threading.Tasks; |
|
|
|
|
|
|
|
@ -22,6 +23,21 @@ namespace ImageProcessorCore.Formats |
|
|
|
/// </summary>
|
|
|
|
private const int MaxBlockSize = 65535; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Contains the raw pixel data from the image.
|
|
|
|
/// </summary>
|
|
|
|
byte[] pixelData; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The image width.
|
|
|
|
/// </summary>
|
|
|
|
private int width; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The image height.
|
|
|
|
/// </summary>
|
|
|
|
private int height; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The number of bits required to encode the colors in the png.
|
|
|
|
/// </summary>
|
|
|
|
@ -32,6 +48,11 @@ namespace ImageProcessorCore.Formats |
|
|
|
/// </summary>
|
|
|
|
public int Quality { get; set; } |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the png color type
|
|
|
|
/// </summary>
|
|
|
|
public PngColorType PngColorType { get; set; } |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The compression level 1-9.
|
|
|
|
/// <remarks>Defaults to 6.</remarks>
|
|
|
|
@ -76,6 +97,9 @@ namespace ImageProcessorCore.Formats |
|
|
|
Guard.NotNull(image, nameof(image)); |
|
|
|
Guard.NotNull(stream, nameof(stream)); |
|
|
|
|
|
|
|
this.width = image.Width; |
|
|
|
this.height = image.Height; |
|
|
|
|
|
|
|
// Write the png header.
|
|
|
|
stream.Write( |
|
|
|
new byte[] |
|
|
|
@ -96,6 +120,13 @@ namespace ImageProcessorCore.Formats |
|
|
|
int quality = this.Quality > 0 ? this.Quality : image.Quality; |
|
|
|
this.Quality = quality > 0 ? quality.Clamp(1, int.MaxValue) : int.MaxValue; |
|
|
|
|
|
|
|
// Set correct color type.
|
|
|
|
if (Quality <= 256) |
|
|
|
{ |
|
|
|
this.PngColorType = PngColorType.Palette; |
|
|
|
} |
|
|
|
|
|
|
|
// Set correct bit depth.
|
|
|
|
this.bitDepth = this.Quality <= 256 |
|
|
|
? (byte)(ImageMaths.GetBitsNeededForColorDepth(this.Quality).Clamp(1, 8)) |
|
|
|
: (byte)8; |
|
|
|
@ -123,19 +154,166 @@ namespace ImageProcessorCore.Formats |
|
|
|
}; |
|
|
|
|
|
|
|
this.WriteHeaderChunk(stream, header); |
|
|
|
QuantizedImage<T, TP> quantized = this.WritePaletteChunk(stream, header, image); |
|
|
|
this.WritePhysicalChunk(stream, image); |
|
|
|
this.WriteGammaChunk(stream); |
|
|
|
|
|
|
|
using (IPixelAccessor<T, TP> pixels = image.Lock()) |
|
|
|
if (this.Quality <= 256) |
|
|
|
{ |
|
|
|
this.WriteDataChunks(stream, pixels, quantized); |
|
|
|
// Quatize the image and get the pixels
|
|
|
|
QuantizedImage<T, TP> quantized = this.WritePaletteChunk(stream, header, image); |
|
|
|
pixelData = quantized.Pixels; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Copy the pixels across from the image.
|
|
|
|
// TODO: This should vary by bytes per pixel.
|
|
|
|
this.pixelData = new byte[this.width * this.height * 4]; |
|
|
|
int stride = this.width * 4; |
|
|
|
using (IPixelAccessor<T, TP> pixels = image.Lock()) |
|
|
|
{ |
|
|
|
for (int y = 0; y < this.height; y++) |
|
|
|
{ |
|
|
|
for (int x = 0; x < this.width; x++) |
|
|
|
{ |
|
|
|
int dataOffset = (y * stride) + (x * 4); |
|
|
|
byte[] source = pixels[x, y].ToBytes(); |
|
|
|
|
|
|
|
// r -> g -> b -> a
|
|
|
|
this.pixelData[dataOffset] = source[0]; |
|
|
|
this.pixelData[dataOffset + 1] = source[1]; |
|
|
|
this.pixelData[dataOffset + 2] = source[2]; |
|
|
|
this.pixelData[dataOffset + 3] = source[3]; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.WritePhysicalChunk(stream, image); |
|
|
|
this.WriteGammaChunk(stream); |
|
|
|
|
|
|
|
//using (IPixelAccessor<T, TP> pixels = image.Lock())
|
|
|
|
//{
|
|
|
|
// this.WriteDataChunks(stream, pixels, quantized);
|
|
|
|
//}
|
|
|
|
this.WriteDataChunks(stream); |
|
|
|
|
|
|
|
this.WriteEndChunk(stream); |
|
|
|
stream.Flush(); |
|
|
|
} |
|
|
|
|
|
|
|
private byte[] EncodePixelData() |
|
|
|
{ |
|
|
|
List<byte[]> filteredScanlines = new List<byte[]>(); |
|
|
|
|
|
|
|
int bytesPerPixel = CalculateBytesPerPixel(); |
|
|
|
byte[] previousScanline = new byte[width * bytesPerPixel]; |
|
|
|
|
|
|
|
for (int y = 0; y < height; y++) |
|
|
|
{ |
|
|
|
byte[] rawScanline = GetRawScanline(y); |
|
|
|
byte[] filteredScanline = GetOptimalFilteredScanline(rawScanline, previousScanline, bytesPerPixel); |
|
|
|
|
|
|
|
filteredScanlines.Add(filteredScanline); |
|
|
|
|
|
|
|
previousScanline = rawScanline; |
|
|
|
} |
|
|
|
|
|
|
|
List<byte> result = new List<byte>(); |
|
|
|
|
|
|
|
foreach (var encodedScanline in filteredScanlines) |
|
|
|
{ |
|
|
|
result.AddRange(encodedScanline); |
|
|
|
} |
|
|
|
|
|
|
|
return result.ToArray(); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Applies all PNG filters to the given scanline and returns the filtered scanline that is deemed
|
|
|
|
/// to be most compressible, using lowest total variation as proxy for compressibility.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="rawScanline"></param>
|
|
|
|
/// <param name="previousScanline"></param>
|
|
|
|
/// <param name="bytesPerPixel"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
private byte[] GetOptimalFilteredScanline(byte[] rawScanline, byte[] previousScanline, int bytesPerPixel) |
|
|
|
{ |
|
|
|
List<Tuple<byte[], int>> candidates = new List<Tuple<byte[], int>>(); |
|
|
|
|
|
|
|
byte[] sub = SubFilter.Encode(rawScanline, bytesPerPixel); |
|
|
|
candidates.Add(new Tuple<byte[], int>(sub, CalculateTotalVariation(sub))); |
|
|
|
|
|
|
|
byte[] up = UpFilter.Encode(rawScanline, previousScanline); |
|
|
|
candidates.Add(new Tuple<byte[], int>(up, CalculateTotalVariation(up))); |
|
|
|
|
|
|
|
byte[] average = AverageFilter.Encode(rawScanline, previousScanline, bytesPerPixel); |
|
|
|
candidates.Add(new Tuple<byte[], int>(average, CalculateTotalVariation(average))); |
|
|
|
|
|
|
|
byte[] paeth = PaethFilter.Encode(rawScanline, previousScanline, bytesPerPixel); |
|
|
|
candidates.Add(new Tuple<byte[], int>(paeth, CalculateTotalVariation(paeth))); |
|
|
|
|
|
|
|
int lowestTotalVariation = int.MaxValue; |
|
|
|
int lowestTotalVariationIndex = 0; |
|
|
|
|
|
|
|
for (int i = 0; i < candidates.Count; i++) |
|
|
|
{ |
|
|
|
if (candidates[i].Item2 < lowestTotalVariation) |
|
|
|
{ |
|
|
|
lowestTotalVariationIndex = i; |
|
|
|
lowestTotalVariation = candidates[i].Item2; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return candidates[lowestTotalVariationIndex].Item1; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Calculates the total variation of given byte array. Total variation is the sum of the absolute values of
|
|
|
|
/// neighbour differences.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="input"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
private int CalculateTotalVariation(byte[] input) |
|
|
|
{ |
|
|
|
int totalVariation = 0; |
|
|
|
|
|
|
|
for (int i = 1; i < input.Length; i++) |
|
|
|
{ |
|
|
|
totalVariation += Math.Abs(input[i] - input[i - 1]); |
|
|
|
} |
|
|
|
|
|
|
|
return totalVariation; |
|
|
|
} |
|
|
|
|
|
|
|
private byte[] GetRawScanline(int y) |
|
|
|
{ |
|
|
|
// TODO: This should vary by bytes per pixel.
|
|
|
|
int stride = (this.PngColorType == PngColorType.Palette ? 1 : 4) * this.width; |
|
|
|
byte[] rawScanline = new byte[stride]; |
|
|
|
Array.Copy(this.pixelData, y * stride, rawScanline, 0, stride); |
|
|
|
return rawScanline; |
|
|
|
} |
|
|
|
|
|
|
|
private int CalculateBytesPerPixel() |
|
|
|
{ |
|
|
|
switch (this.PngColorType) |
|
|
|
{ |
|
|
|
case PngColorType.Grayscale: |
|
|
|
return 1; |
|
|
|
|
|
|
|
case PngColorType.GrayscaleWithAlpha: |
|
|
|
return 2; |
|
|
|
|
|
|
|
case PngColorType.Palette: |
|
|
|
return 1; |
|
|
|
|
|
|
|
case PngColorType.Rgb: |
|
|
|
return 3; |
|
|
|
|
|
|
|
// PngColorType.RgbWithAlpha
|
|
|
|
default: |
|
|
|
return 4; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Writes an integer to the byte array.
|
|
|
|
/// </summary>
|
|
|
|
@ -313,94 +491,9 @@ namespace ImageProcessorCore.Formats |
|
|
|
/// <summary>
|
|
|
|
/// Writes the pixel information to the stream.
|
|
|
|
/// </summary>
|
|
|
|
/// <typeparam name="T">The pixel format.</typeparam>
|
|
|
|
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
|
|
|
|
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
|
|
|
|
/// <param name="pixels">The image pixels.</param>
|
|
|
|
/// <param name="quantized">The quantized image.</param>
|
|
|
|
private void WriteDataChunks<T, TP>(Stream stream, IPixelAccessor<T, TP> pixels, QuantizedImage<T, TP> quantized) |
|
|
|
where T : IPackedVector<TP> |
|
|
|
where TP : struct |
|
|
|
private void WriteDataChunks(Stream stream) |
|
|
|
{ |
|
|
|
byte[] data; |
|
|
|
int imageWidth = pixels.Width; |
|
|
|
int imageHeight = pixels.Height; |
|
|
|
|
|
|
|
// Indexed image.
|
|
|
|
if (this.Quality <= 256) |
|
|
|
{ |
|
|
|
int rowLength = imageWidth + 1; |
|
|
|
data = new byte[rowLength * imageHeight]; |
|
|
|
|
|
|
|
Parallel.For( |
|
|
|
0, |
|
|
|
imageHeight, |
|
|
|
Bootstrapper.Instance.ParallelOptions, |
|
|
|
y => |
|
|
|
{ |
|
|
|
int dataOffset = (y * rowLength); |
|
|
|
byte compression = 0; |
|
|
|
if (y > 0) |
|
|
|
{ |
|
|
|
compression = 2; |
|
|
|
} |
|
|
|
data[dataOffset++] = compression; |
|
|
|
for (int x = 0; x < imageWidth; x++) |
|
|
|
{ |
|
|
|
data[dataOffset++] = quantized.Pixels[(y * imageWidth) + x]; |
|
|
|
if (y > 0) |
|
|
|
{ |
|
|
|
data[dataOffset - 1] -= quantized.Pixels[((y - 1) * imageWidth) + x]; |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// TrueColor image.
|
|
|
|
data = new byte[(imageWidth * imageHeight * 4) + pixels.Height]; |
|
|
|
|
|
|
|
int rowLength = (imageWidth * 4) + 1; |
|
|
|
|
|
|
|
Parallel.For( |
|
|
|
0, |
|
|
|
imageHeight, |
|
|
|
Bootstrapper.Instance.ParallelOptions, |
|
|
|
y => |
|
|
|
{ |
|
|
|
byte compression = 0; |
|
|
|
if (y > 0) |
|
|
|
{ |
|
|
|
compression = 2; |
|
|
|
} |
|
|
|
|
|
|
|
data[y * rowLength] = compression; |
|
|
|
|
|
|
|
for (int x = 0; x < imageWidth; x++) |
|
|
|
{ |
|
|
|
byte[] color = pixels[x, y].ToBytes(); |
|
|
|
|
|
|
|
// Calculate the offset for the new array.
|
|
|
|
int dataOffset = (y * rowLength) + (x * 4) + 1; |
|
|
|
|
|
|
|
// Expected format
|
|
|
|
data[dataOffset] = color[0]; |
|
|
|
data[dataOffset + 1] = color[1]; |
|
|
|
data[dataOffset + 2] = color[2]; |
|
|
|
data[dataOffset + 3] = color[3]; |
|
|
|
|
|
|
|
if (y > 0) |
|
|
|
{ |
|
|
|
color = pixels[x, y - 1].ToBytes(); |
|
|
|
|
|
|
|
data[dataOffset] -= color[0]; |
|
|
|
data[dataOffset + 1] -= color[1]; |
|
|
|
data[dataOffset + 2] -= color[2]; |
|
|
|
data[dataOffset + 3] -= color[3]; |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
byte[] data = this.EncodePixelData(); |
|
|
|
|
|
|
|
byte[] buffer; |
|
|
|
int bufferLength; |
|
|
|
@ -443,6 +536,139 @@ namespace ImageProcessorCore.Formats |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
///// <summary>
|
|
|
|
///// Writes the pixel information to the stream.
|
|
|
|
///// </summary>
|
|
|
|
///// <typeparam name="T">The pixel format.</typeparam>
|
|
|
|
///// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
|
|
|
|
///// <param name="stream">The <see cref="Stream"/> containing image data.</param>
|
|
|
|
///// <param name="pixels">The image pixels.</param>
|
|
|
|
///// <param name="quantized">The quantized image.</param>
|
|
|
|
//private void WriteDataChunks<T, TP>(Stream stream, IPixelAccessor<T, TP> pixels, QuantizedImage<T, TP> quantized)
|
|
|
|
// where T : IPackedVector<TP>
|
|
|
|
// where TP : struct
|
|
|
|
//{
|
|
|
|
// byte[] data;
|
|
|
|
// int imageWidth = pixels.Width;
|
|
|
|
// int imageHeight = pixels.Height;
|
|
|
|
|
|
|
|
// // Indexed image.
|
|
|
|
// if (this.Quality <= 256)
|
|
|
|
// {
|
|
|
|
// int rowLength = imageWidth + 1;
|
|
|
|
// data = new byte[rowLength * imageHeight];
|
|
|
|
|
|
|
|
// Parallel.For(
|
|
|
|
// 0,
|
|
|
|
// imageHeight,
|
|
|
|
// Bootstrapper.Instance.ParallelOptions,
|
|
|
|
// y =>
|
|
|
|
// {
|
|
|
|
// int dataOffset = (y * rowLength);
|
|
|
|
// byte compression = 0;
|
|
|
|
// if (y > 0)
|
|
|
|
// {
|
|
|
|
// compression = 2;
|
|
|
|
// }
|
|
|
|
// data[dataOffset++] = compression;
|
|
|
|
// for (int x = 0; x < imageWidth; x++)
|
|
|
|
// {
|
|
|
|
// data[dataOffset++] = quantized.Pixels[(y * imageWidth) + x];
|
|
|
|
// if (y > 0)
|
|
|
|
// {
|
|
|
|
// data[dataOffset - 1] -= quantized.Pixels[((y - 1) * imageWidth) + x];
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// });
|
|
|
|
// }
|
|
|
|
// else
|
|
|
|
// {
|
|
|
|
// // TrueColor image.
|
|
|
|
// data = new byte[(imageWidth * imageHeight * 4) + pixels.Height];
|
|
|
|
|
|
|
|
// int rowLength = (imageWidth * 4) + 1;
|
|
|
|
|
|
|
|
// Parallel.For(
|
|
|
|
// 0,
|
|
|
|
// imageHeight,
|
|
|
|
// Bootstrapper.Instance.ParallelOptions,
|
|
|
|
// y =>
|
|
|
|
// {
|
|
|
|
// byte compression = 0;
|
|
|
|
// if (y > 0)
|
|
|
|
// {
|
|
|
|
// compression = 2;
|
|
|
|
// }
|
|
|
|
|
|
|
|
// data[y * rowLength] = compression;
|
|
|
|
|
|
|
|
// for (int x = 0; x < imageWidth; x++)
|
|
|
|
// {
|
|
|
|
// byte[] color = pixels[x, y].ToBytes();
|
|
|
|
|
|
|
|
// // Calculate the offset for the new array.
|
|
|
|
// int dataOffset = (y * rowLength) + (x * 4) + 1;
|
|
|
|
|
|
|
|
// // Expected format
|
|
|
|
// data[dataOffset] = color[0];
|
|
|
|
// data[dataOffset + 1] = color[1];
|
|
|
|
// data[dataOffset + 2] = color[2];
|
|
|
|
// data[dataOffset + 3] = color[3];
|
|
|
|
|
|
|
|
// if (y > 0)
|
|
|
|
// {
|
|
|
|
// color = pixels[x, y - 1].ToBytes();
|
|
|
|
|
|
|
|
// data[dataOffset] -= color[0];
|
|
|
|
// data[dataOffset + 1] -= color[1];
|
|
|
|
// data[dataOffset + 2] -= color[2];
|
|
|
|
// data[dataOffset + 3] -= color[3];
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// });
|
|
|
|
// }
|
|
|
|
|
|
|
|
// byte[] buffer;
|
|
|
|
// int bufferLength;
|
|
|
|
|
|
|
|
// MemoryStream memoryStream = null;
|
|
|
|
// try
|
|
|
|
// {
|
|
|
|
// memoryStream = new MemoryStream();
|
|
|
|
|
|
|
|
// using (ZlibDeflateStream deflateStream = new ZlibDeflateStream(memoryStream, this.CompressionLevel))
|
|
|
|
// {
|
|
|
|
// deflateStream.Write(data, 0, data.Length);
|
|
|
|
// }
|
|
|
|
|
|
|
|
// bufferLength = (int)memoryStream.Length;
|
|
|
|
// buffer = memoryStream.ToArray();
|
|
|
|
// }
|
|
|
|
// finally
|
|
|
|
// {
|
|
|
|
// memoryStream?.Dispose();
|
|
|
|
// }
|
|
|
|
|
|
|
|
// int numChunks = bufferLength / MaxBlockSize;
|
|
|
|
|
|
|
|
// if (bufferLength % MaxBlockSize != 0)
|
|
|
|
// {
|
|
|
|
// numChunks++;
|
|
|
|
// }
|
|
|
|
|
|
|
|
// for (int i = 0; i < numChunks; i++)
|
|
|
|
// {
|
|
|
|
// int length = bufferLength - (i * MaxBlockSize);
|
|
|
|
|
|
|
|
// if (length > MaxBlockSize)
|
|
|
|
// {
|
|
|
|
// length = MaxBlockSize;
|
|
|
|
// }
|
|
|
|
|
|
|
|
// this.WriteChunk(stream, PngChunkTypes.Data, buffer, i * MaxBlockSize, length);
|
|
|
|
// }
|
|
|
|
//}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Writes the chunk end to the stream.
|
|
|
|
/// </summary>
|
|
|
|
|