mirror of
https://github.com/danbulant/Cosmos
synced 2026-05-19 20:39:01 +00:00
1304 lines
48 KiB
C#
1304 lines
48 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using System.IO;
|
|
using Orvid.Compression.Streams;
|
|
using Orvid.Compression.Checksums;
|
|
|
|
namespace Orvid.Graphics.ImageFormats
|
|
{
|
|
public class PngImage : ImageFormat
|
|
{
|
|
public override void Save(Image i, Stream dest)
|
|
{
|
|
PngEncoder p = new PngEncoder();
|
|
p.Encode(i, dest);
|
|
}
|
|
|
|
public override Image Load(Stream s)
|
|
{
|
|
PngDecoder p = new PngDecoder();
|
|
return p.Decode(s);
|
|
}
|
|
|
|
// Please note, everything below this
|
|
// point was originally from the ImageTools
|
|
// Library, available here:
|
|
// http://imagetools.codeplex.com/
|
|
//
|
|
//
|
|
// The source has been modified for use in this library.
|
|
//
|
|
// This disclaimer was last
|
|
// modified on August 9, 2011.
|
|
|
|
#region Encoder
|
|
/// <summary>
|
|
/// Image encoder for writing image data to a stream in png format.
|
|
/// </summary>
|
|
private class PngEncoder
|
|
{
|
|
private const int MaxBlockSize = 0xFFFF;
|
|
private Stream _stream;
|
|
private Image _image;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this encoder
|
|
/// will write the image uncompressed the stream.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if the image should be written uncompressed to
|
|
/// the stream; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool IsWritingUncompressed { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this instance is writing
|
|
/// gamma information to the stream. The default value is false.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if this instance is writing gamma
|
|
/// information to the stream.; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool IsWritingGamma { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the gamma value, that will be written
|
|
/// the the stream, when the <see cref="IsWritingGamma"/> property
|
|
/// is set to true. The default value is 2.2f.
|
|
/// </summary>
|
|
/// <value>The gamma value of the image.</value>
|
|
public double Gamma { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets the default file extension for this encoder.
|
|
/// </summary>
|
|
/// <value>The default file extension for this encoder.</value>
|
|
public string Extension
|
|
{
|
|
get { return "PNG"; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PngEncoder"/> class.
|
|
/// </summary>
|
|
public PngEncoder()
|
|
{
|
|
Gamma = 2.2f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates if the image encoder supports the specified
|
|
/// file extension.
|
|
/// </summary>
|
|
/// <param name="extension">The file extension.</param>
|
|
/// <returns><c>true</c>, if the encoder supports the specified
|
|
/// extensions; otherwise <c>false</c>.
|
|
/// </returns>
|
|
/// <exception cref="ArgumentNullException"><paramref name="extension"/>
|
|
/// is null (Nothing in Visual Basic).</exception>
|
|
/// <exception cref="ArgumentException"><paramref name="extension"/> is a string
|
|
/// of length zero or contains only blanks.</exception>
|
|
public bool IsSupportedFileExtension(string extension)
|
|
{
|
|
string extensionAsUpper = extension.ToUpper(System.Globalization.CultureInfo.CurrentCulture);
|
|
return extensionAsUpper == "PNG";
|
|
}
|
|
|
|
public void Encode(Image image, Stream stream)
|
|
{
|
|
_image = image;
|
|
_stream = stream;
|
|
// Write the png header.
|
|
stream.Write(
|
|
new byte[]
|
|
{
|
|
0x89, 0x50, 0x4E, 0x47,
|
|
0x0D, 0x0A, 0x1A, 0x0A
|
|
}, 0, 8);
|
|
|
|
PngHeader header = new PngHeader();
|
|
header.Width = image.Width;
|
|
header.Height = image.Height;
|
|
header.ColorType = 6;
|
|
header.BitDepth = 8;
|
|
header.FilterMethod = 0;
|
|
header.CompressionMethod = 0;
|
|
header.InterlaceMethod = 0;
|
|
|
|
WriteHeaderChunk(header);
|
|
|
|
WritePhysicsChunk();
|
|
WriteGammaChunk();
|
|
|
|
if (IsWritingUncompressed)
|
|
{
|
|
WriteDataChunksFast();
|
|
}
|
|
else
|
|
{
|
|
WriteDataChunks();
|
|
}
|
|
WriteEndChunk();
|
|
|
|
stream.Flush();
|
|
}
|
|
|
|
private void WritePhysicsChunk()
|
|
{
|
|
//if (_image.DensityX > 0 && _image.DensityY > 0)
|
|
{
|
|
#warning TODO: Un-Comment this when DPI is supported in Orvid.Graphics.Image
|
|
//int dpmX = (int)Math.Round(_image.DensityX * 39.3700787d);
|
|
//int dpmY = (int)Math.Round(_image.DensityY * 39.3700787d);
|
|
|
|
// DPI isn't currently supported,
|
|
// so used 75 as the default.
|
|
int dpmX = 2953;
|
|
int dpmY = 2953;
|
|
|
|
byte[] chunkData = new byte[9];
|
|
|
|
WriteInteger(chunkData, 0, dpmX);
|
|
WriteInteger(chunkData, 4, dpmY);
|
|
|
|
chunkData[8] = 1;
|
|
|
|
WriteChunk(PngChunkTypes.Physical, chunkData);
|
|
}
|
|
}
|
|
|
|
private void WriteGammaChunk()
|
|
{
|
|
if (IsWritingGamma)
|
|
{
|
|
int gammeValue = (int)(Gamma * 100000f);
|
|
|
|
byte[] fourByteData = new byte[4];
|
|
|
|
byte[] size = BitConverter.GetBytes(gammeValue);
|
|
fourByteData[0] = size[3]; fourByteData[1] = size[2]; fourByteData[2] = size[1]; fourByteData[3] = size[0];
|
|
|
|
WriteChunk(PngChunkTypes.Gamma, fourByteData);
|
|
}
|
|
}
|
|
|
|
private void WriteDataChunksFast()
|
|
{
|
|
byte[] pixels = ConvertPixelArrayToByteArray(_image.Data);
|
|
|
|
// Convert the pixel array to a new array for adding
|
|
// the filter byte.
|
|
// --------------------------------------------------
|
|
byte[] data = new byte[_image.Width * _image.Height * 4 + _image.Height];
|
|
|
|
int rowLength = _image.Width * 4 + 1;
|
|
|
|
for (int y = 0; y < _image.Height; y++)
|
|
{
|
|
data[y * rowLength] = 0;
|
|
|
|
Array.Copy(pixels, y * _image.Width * 4, data, y * rowLength + 1, _image.Width * 4);
|
|
}
|
|
// --------------------------------------------------
|
|
|
|
Adler32 adler32 = new Adler32();
|
|
adler32.Update(data);
|
|
|
|
using (MemoryStream tempStream = new MemoryStream())
|
|
{
|
|
int remainder = data.Length;
|
|
|
|
int blockCount;
|
|
if ((data.Length % MaxBlockSize) == 0)
|
|
{
|
|
blockCount = data.Length / MaxBlockSize;
|
|
}
|
|
else
|
|
{
|
|
blockCount = (data.Length / MaxBlockSize) + 1;
|
|
}
|
|
|
|
// Write headers
|
|
tempStream.WriteByte(0x78);
|
|
tempStream.WriteByte(0xDA);
|
|
|
|
for (int i = 0; i < blockCount; i++)
|
|
{
|
|
// Write the length
|
|
ushort length = (ushort)((remainder < MaxBlockSize) ? remainder : MaxBlockSize);
|
|
|
|
if (length == remainder)
|
|
{
|
|
tempStream.WriteByte(0x01);
|
|
}
|
|
else
|
|
{
|
|
tempStream.WriteByte(0x00);
|
|
}
|
|
|
|
tempStream.Write(BitConverter.GetBytes(length), 0, 2);
|
|
|
|
// Write one's compliment of length
|
|
tempStream.Write(BitConverter.GetBytes((ushort)~length), 0, 2);
|
|
|
|
// Write blocks
|
|
tempStream.Write(data, (int)(i * MaxBlockSize), length);
|
|
|
|
// Next block
|
|
remainder -= MaxBlockSize;
|
|
}
|
|
|
|
WriteInteger(tempStream, (int)adler32.Value);
|
|
|
|
tempStream.Seek(0, SeekOrigin.Begin);
|
|
|
|
byte[] zipData = new byte[tempStream.Length];
|
|
tempStream.Read(zipData, 0, (int)tempStream.Length);
|
|
|
|
WriteChunk(PngChunkTypes.Data, zipData);
|
|
}
|
|
}
|
|
|
|
private byte[] ConvertPixelArrayToByteArray(Pixel[] a)
|
|
{
|
|
byte[] b = new byte[a.Length * 4];
|
|
|
|
int indx = 0;
|
|
Pixel p;
|
|
for (uint i = 0; i < a.Length; i++)
|
|
{
|
|
p = a[i];
|
|
b[indx] = p.R;
|
|
indx++;
|
|
b[indx] = p.G;
|
|
indx++;
|
|
b[indx] = p.B;
|
|
indx++;
|
|
b[indx] = p.A;
|
|
indx++;
|
|
}
|
|
|
|
return b;
|
|
}
|
|
|
|
private void WriteDataChunks()
|
|
{
|
|
byte[] pixels = ConvertPixelArrayToByteArray(_image.Data);
|
|
|
|
byte[] data = new byte[_image.Width * _image.Height * 4 + _image.Height];
|
|
|
|
int rowLength = _image.Width * 4 + 1;
|
|
|
|
for (int y = 0; y < _image.Height; y++)
|
|
{
|
|
byte compression = 0;
|
|
if (y > 0)
|
|
{
|
|
compression = 2;
|
|
}
|
|
data[y * rowLength] = compression;
|
|
|
|
for (int x = 0; x < _image.Width; x++)
|
|
{
|
|
// Calculate the offset for the new array.
|
|
int dataOffset = y * rowLength + x * 4 + 1;
|
|
|
|
// Calculate the offset for the original pixel array.
|
|
int pixelOffset = (y * _image.Width + x) * 4;
|
|
|
|
data[dataOffset + 0] = pixels[pixelOffset + 0];
|
|
data[dataOffset + 1] = pixels[pixelOffset + 1];
|
|
data[dataOffset + 2] = pixels[pixelOffset + 2];
|
|
data[dataOffset + 3] = pixels[pixelOffset + 3];
|
|
|
|
if (y > 0)
|
|
{
|
|
int lastOffset = ((y - 1) * _image.Width + x) * 4;
|
|
|
|
data[dataOffset + 0] -= pixels[lastOffset + 0];
|
|
data[dataOffset + 1] -= pixels[lastOffset + 1];
|
|
data[dataOffset + 2] -= pixels[lastOffset + 2];
|
|
data[dataOffset + 3] -= pixels[lastOffset + 3];
|
|
}
|
|
}
|
|
}
|
|
|
|
byte[] buffer = null;
|
|
int bufferLength = 0;
|
|
|
|
MemoryStream memoryStream = null;
|
|
try
|
|
{
|
|
memoryStream = new MemoryStream();
|
|
using (DeflaterOutputStream zStream = new DeflaterOutputStream(memoryStream))
|
|
{
|
|
memoryStream = null;
|
|
|
|
zStream.Write(data, 0, data.Length);
|
|
zStream.Flush();
|
|
zStream.Finish();
|
|
|
|
bufferLength = (int)memoryStream.Length;
|
|
buffer = memoryStream.GetBuffer();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (memoryStream != null)
|
|
{
|
|
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;
|
|
}
|
|
|
|
WriteChunk(PngChunkTypes.Data, buffer, i * MaxBlockSize, length);
|
|
}
|
|
}
|
|
|
|
private void WriteEndChunk()
|
|
{
|
|
WriteChunk(PngChunkTypes.End, null);
|
|
}
|
|
|
|
private void WriteHeaderChunk(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;
|
|
|
|
WriteChunk(PngChunkTypes.Header, chunkData);
|
|
}
|
|
|
|
private void WriteChunk(string type, byte[] data)
|
|
{
|
|
WriteChunk(type, data, 0, data != null ? data.Length : 0);
|
|
}
|
|
|
|
private void WriteChunk(string type, byte[] data, int offset, int length)
|
|
{
|
|
WriteInteger(_stream, length);
|
|
|
|
byte[] typeArray = new byte[4];
|
|
typeArray[0] = (byte)type[0];
|
|
typeArray[1] = (byte)type[1];
|
|
typeArray[2] = (byte)type[2];
|
|
typeArray[3] = (byte)type[3];
|
|
|
|
_stream.Write(typeArray, 0, 4);
|
|
|
|
if (data != null)
|
|
{
|
|
_stream.Write(data, offset, length);
|
|
}
|
|
|
|
Crc32 crc32 = new Crc32();
|
|
crc32.Update(typeArray);
|
|
|
|
if (data != null)
|
|
{
|
|
crc32.Update(data, offset, length);
|
|
}
|
|
|
|
WriteInteger(_stream, (uint)crc32.Value);
|
|
}
|
|
|
|
private static void WriteInteger(byte[] data, int offset, int value)
|
|
{
|
|
byte[] buffer = BitConverter.GetBytes(value);
|
|
|
|
Array.Reverse(buffer);
|
|
Array.Copy(buffer, 0, data, offset, 4);
|
|
}
|
|
|
|
private static void WriteInteger(Stream stream, int value)
|
|
{
|
|
byte[] buffer = BitConverter.GetBytes(value);
|
|
|
|
Array.Reverse(buffer);
|
|
|
|
stream.Write(buffer, 0, 4);
|
|
}
|
|
|
|
private static void WriteInteger(Stream stream, uint value)
|
|
{
|
|
byte[] buffer = BitConverter.GetBytes(value);
|
|
|
|
Array.Reverse(buffer);
|
|
|
|
stream.Write(buffer, 0, 4);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Decoder
|
|
private class PngDecoder
|
|
{
|
|
private static readonly Dictionary<int, PngColorTypeInformation> _colorTypes = new Dictionary<int, PngColorTypeInformation>();
|
|
private Stream _stream;
|
|
private PngHeader _header;
|
|
|
|
static PngDecoder()
|
|
{
|
|
_colorTypes.Add(0,
|
|
new PngColorTypeInformation(1, new int[] { 1, 2, 4, 8 },
|
|
(p, a) => new GrayscaleReader(false)));
|
|
|
|
_colorTypes.Add(2,
|
|
new PngColorTypeInformation(3, new int[] { 8 },
|
|
(p, a) => new TrueColorReader(false)));
|
|
|
|
_colorTypes.Add(3,
|
|
new PngColorTypeInformation(1, new int[] { 1, 2, 4, 8 },
|
|
(p, a) => new PaletteIndexReader(p, a)));
|
|
|
|
_colorTypes.Add(4,
|
|
new PngColorTypeInformation(2, new int[] { 8 },
|
|
(p, a) => new GrayscaleReader(true)));
|
|
|
|
_colorTypes.Add(6,
|
|
new PngColorTypeInformation(4, new int[] { 8 },
|
|
(p, a) => new TrueColorReader(true)));
|
|
}
|
|
|
|
#region IImageDecoder Members
|
|
|
|
/// <summary>
|
|
/// Gets the size of the header for this image type.
|
|
/// </summary>
|
|
/// <value>The size of the header.</value>
|
|
public int HeaderSize
|
|
{
|
|
get { return 8; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates if the image decoder supports the specified
|
|
/// file extension.
|
|
/// </summary>
|
|
/// <param name="extension">The file extension.</param>
|
|
/// <returns>
|
|
/// <c>true</c>, if the decoder supports the specified
|
|
/// extensions; otherwise <c>false</c>.
|
|
/// </returns>
|
|
/// <exception cref="ArgumentNullException"><paramref name="extension"/>
|
|
/// is null (Nothing in Visual Basic).</exception>
|
|
/// <exception cref="ArgumentException"><paramref name="extension"/> is a string
|
|
/// of length zero or contains only blanks.</exception>
|
|
public bool IsSupportedFileExtension(string extension)
|
|
{
|
|
string extensionAsUpper = extension.ToUpper(System.Globalization.CultureInfo.CurrentCulture);
|
|
return extensionAsUpper == "PNG";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates if the image decoder supports the specified
|
|
/// file header.
|
|
/// </summary>
|
|
/// <param name="header">The file header.</param>
|
|
/// <returns>
|
|
/// <c>true</c>, if the decoder supports the specified
|
|
/// file header; otherwise <c>false</c>.
|
|
/// </returns>
|
|
/// <exception cref="ArgumentNullException"><paramref name="header"/>
|
|
/// is null (Nothing in Visual Basic).</exception>
|
|
public bool IsSupportedFileFormat(byte[] header)
|
|
{
|
|
bool isPng = false;
|
|
|
|
if (header.Length >= 8)
|
|
{
|
|
isPng =
|
|
header[0] == 0x89 &&
|
|
header[1] == 0x50 && // P
|
|
header[2] == 0x4E && // N
|
|
header[3] == 0x47 && // G
|
|
header[4] == 0x0D && // CR
|
|
header[5] == 0x0A && // LF
|
|
header[6] == 0x1A && // EOF
|
|
header[7] == 0x0A; // LF
|
|
}
|
|
|
|
return isPng;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decodes the image from the specified stream and sets
|
|
/// the data to image.
|
|
/// </summary>
|
|
/// <param name="image">The image, where the data should be set to.
|
|
/// Cannot be null (Nothing in Visual Basic).</param>
|
|
/// <param name="stream">The stream, where the image should be
|
|
/// decoded from. Cannot be null (Nothing in Visual Basic).</param>
|
|
/// <exception cref="ArgumentNullException">
|
|
/// <para><paramref name="image"/> is null (Nothing in Visual Basic).</para>
|
|
/// <para>- or -</para>
|
|
/// <para><paramref name="stream"/> is null (Nothing in Visual Basic).</para>
|
|
/// </exception>
|
|
public Image Decode(Stream stream)
|
|
{
|
|
_stream = stream;
|
|
_stream.Seek(8, SeekOrigin.Current);
|
|
|
|
bool isEndChunckReached = false;
|
|
|
|
PngChunk currentChunk = null;
|
|
|
|
byte[] palette = null;
|
|
byte[] paletteAlpha = null;
|
|
|
|
using (MemoryStream dataStream = new MemoryStream())
|
|
{
|
|
while ((currentChunk = ReadChunk()) != null)
|
|
{
|
|
if (isEndChunckReached)
|
|
{
|
|
throw new Exception("Image does not end with end chunk.");
|
|
}
|
|
|
|
if (currentChunk.Type == PngChunkTypes.Header)
|
|
{
|
|
ReadHeaderChunk(currentChunk.Data);
|
|
|
|
ValidateHeader();
|
|
}
|
|
else if (currentChunk.Type == PngChunkTypes.Physical)
|
|
{
|
|
ReadPhysicalChunk(currentChunk.Data);
|
|
}
|
|
else if (currentChunk.Type == PngChunkTypes.Data)
|
|
{
|
|
dataStream.Write(currentChunk.Data, 0, currentChunk.Data.Length);
|
|
}
|
|
else if (currentChunk.Type == PngChunkTypes.Palette)
|
|
{
|
|
palette = currentChunk.Data;
|
|
}
|
|
else if (currentChunk.Type == PngChunkTypes.PaletteAlpha)
|
|
{
|
|
paletteAlpha = currentChunk.Data;
|
|
}
|
|
else if (currentChunk.Type == PngChunkTypes.Text)
|
|
{
|
|
ReadTextChunk(currentChunk.Data);
|
|
}
|
|
else if (currentChunk.Type == PngChunkTypes.End)
|
|
{
|
|
isEndChunckReached = true;
|
|
}
|
|
}
|
|
|
|
byte[] pixels = new byte[_header.Width * _header.Height * 4];
|
|
|
|
PngColorTypeInformation colorTypeInformation = _colorTypes[_header.ColorType];
|
|
|
|
if (colorTypeInformation != null)
|
|
{
|
|
ColorReader colorReader = colorTypeInformation.CreateColorReader(palette, paletteAlpha);
|
|
|
|
ReadScanlines(dataStream, pixels, colorReader, colorTypeInformation);
|
|
}
|
|
Image i = new Image(_header.Width, _header.Height);
|
|
int indx = 0;
|
|
byte r, g, b, a;
|
|
for (uint y = 0; y < i.Height; y++)
|
|
{
|
|
for (uint x = 0; x < i.Width; x++)
|
|
{
|
|
r = pixels[indx];
|
|
indx++;
|
|
g = pixels[indx];
|
|
indx++;
|
|
b = pixels[indx];
|
|
indx++;
|
|
a = pixels[indx];
|
|
indx++;
|
|
i.SetPixel(x, y, new Pixel(r, g, b, a));
|
|
}
|
|
}
|
|
pixels = null;
|
|
System.GC.Collect();
|
|
return i;
|
|
}
|
|
}
|
|
|
|
private void ReadPhysicalChunk(byte[] data)
|
|
{
|
|
Array.Reverse(data, 0, 4);
|
|
Array.Reverse(data, 4, 4);
|
|
|
|
//_image.DensityX = BitConverter.ToInt32(data, 0) / 39.3700787d;
|
|
//_image.DensityY = BitConverter.ToInt32(data, 4) / 39.3700787d;
|
|
}
|
|
|
|
private int CalculateScanlineLength(PngColorTypeInformation colorTypeInformation)
|
|
{
|
|
int scanlineLength = (_header.Width * _header.BitDepth * colorTypeInformation.ChannelsPerColor);
|
|
|
|
int amount = scanlineLength % 8;
|
|
if (amount != 0)
|
|
{
|
|
scanlineLength += 8 - amount;
|
|
}
|
|
|
|
return scanlineLength / 8;
|
|
}
|
|
|
|
private int CalculateScanlineStep(PngColorTypeInformation colorTypeInformation)
|
|
{
|
|
int scanlineStep = 1;
|
|
|
|
if (_header.BitDepth >= 8)
|
|
{
|
|
scanlineStep = (colorTypeInformation.ChannelsPerColor * _header.BitDepth) / 8;
|
|
}
|
|
|
|
return scanlineStep;
|
|
}
|
|
|
|
private void ReadScanlines(MemoryStream dataStream, byte[] pixels, ColorReader colorReader, PngColorTypeInformation colorTypeInformation)
|
|
{
|
|
dataStream.Position = 0;
|
|
|
|
int scanlineLength = CalculateScanlineLength(colorTypeInformation);
|
|
|
|
int scanlineStep = CalculateScanlineStep(colorTypeInformation);
|
|
|
|
byte[] lastScanline = new byte[scanlineLength];
|
|
byte[] currScanline = new byte[scanlineLength];
|
|
|
|
byte a = 0;
|
|
byte b = 0;
|
|
byte c = 0;
|
|
|
|
int row = 0, filter = 0, column = -1;
|
|
|
|
using (DeflaterInputStream compressedStream = new DeflaterInputStream(dataStream))
|
|
{
|
|
int readByte = 0;
|
|
while ((readByte = compressedStream.ReadByte()) >= 0)
|
|
{
|
|
if (column == -1)
|
|
{
|
|
filter = readByte;
|
|
|
|
column++;
|
|
}
|
|
else
|
|
{
|
|
currScanline[column] = (byte)readByte;
|
|
|
|
if (column >= scanlineStep)
|
|
{
|
|
a = currScanline[column - scanlineStep];
|
|
c = lastScanline[column - scanlineStep];
|
|
}
|
|
else
|
|
{
|
|
a = 0;
|
|
c = 0;
|
|
}
|
|
|
|
b = lastScanline[column];
|
|
|
|
if (filter == 1)
|
|
{
|
|
currScanline[column] = (byte)(currScanline[column] + a);
|
|
}
|
|
else if (filter == 2)
|
|
{
|
|
currScanline[column] = (byte)(currScanline[column] + b);
|
|
}
|
|
else if (filter == 3)
|
|
{
|
|
currScanline[column] = (byte)(currScanline[column] + (byte)Math.Floor((double)((a + b) / 2)));
|
|
}
|
|
else if (filter == 4)
|
|
{
|
|
currScanline[column] = (byte)(currScanline[column] + PaethPredicator(a, b, c));
|
|
}
|
|
|
|
column++;
|
|
|
|
if (column == scanlineLength)
|
|
{
|
|
colorReader.ReadScanline(currScanline, pixels, _header);
|
|
|
|
column = -1;
|
|
row++;
|
|
|
|
Swap(ref currScanline, ref lastScanline);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Swaps two references.
|
|
/// </summary>
|
|
/// <typeparam name="TRef">The type of the references to swap.</typeparam>
|
|
/// <param name="lhs">The first reference.</param>
|
|
/// <param name="rhs">The second reference.</param>
|
|
private static void Swap<TRef>(ref TRef lhs, ref TRef rhs) where TRef : class
|
|
{
|
|
TRef tmp = lhs;
|
|
lhs = rhs;
|
|
rhs = tmp;
|
|
}
|
|
|
|
private static byte PaethPredicator(byte a, byte b, byte c)
|
|
{
|
|
byte predicator = 0;
|
|
|
|
int p = a + b - c;
|
|
int pa = Math.Abs(p - a);
|
|
int pb = Math.Abs(p - b);
|
|
int pc = Math.Abs(p - c);
|
|
|
|
if (pa <= pb && pa <= pc)
|
|
{
|
|
predicator = a;
|
|
}
|
|
else if (pb <= pc)
|
|
{
|
|
predicator = b;
|
|
}
|
|
else
|
|
{
|
|
predicator = c;
|
|
}
|
|
|
|
return predicator;
|
|
}
|
|
|
|
private void ReadTextChunk(byte[] data)
|
|
{
|
|
//int zeroIndex = 0;
|
|
|
|
//for (int i = 0; i < data.Length; i++)
|
|
//{
|
|
// if (data[i] == (byte)0)
|
|
// {
|
|
// zeroIndex = i;
|
|
// break;
|
|
// }
|
|
//}
|
|
|
|
//string name = Encoding.Unicode.GetString(data, 0, zeroIndex);
|
|
//string value = Encoding.Unicode.GetString(data, zeroIndex + 1, data.Length - zeroIndex - 1);
|
|
|
|
//_image.Properties.Add(new ImageProperty(name, value));
|
|
}
|
|
|
|
private void ReadHeaderChunk(byte[] data)
|
|
{
|
|
_header = new PngHeader();
|
|
|
|
Array.Reverse(data, 0, 4);
|
|
Array.Reverse(data, 4, 4);
|
|
|
|
_header.Width = BitConverter.ToInt32(data, 0);
|
|
_header.Height = BitConverter.ToInt32(data, 4);
|
|
|
|
_header.BitDepth = data[8];
|
|
_header.ColorType = data[9];
|
|
_header.FilterMethod = data[11];
|
|
_header.InterlaceMethod = data[12];
|
|
_header.CompressionMethod = data[10];
|
|
}
|
|
|
|
private void ValidateHeader()
|
|
{
|
|
if (!_colorTypes.ContainsKey(_header.ColorType))
|
|
{
|
|
throw new Exception("Color type is not supported or not valid.");
|
|
}
|
|
|
|
bool found = false;
|
|
foreach (int i in _colorTypes[_header.ColorType].SupportedBitDepths)
|
|
{
|
|
if (i == _header.BitDepth)
|
|
{
|
|
found = true;
|
|
continue;
|
|
}
|
|
}
|
|
if (!found)
|
|
{
|
|
throw new Exception("Bit depth is not supported or not valid.");
|
|
}
|
|
|
|
if (_header.FilterMethod != 0)
|
|
{
|
|
throw new Exception("The png specification only defines 0 as filter method.");
|
|
}
|
|
|
|
if (_header.InterlaceMethod != 0)
|
|
{
|
|
throw new Exception("Interlacing is not supported.");
|
|
}
|
|
}
|
|
|
|
private PngChunk ReadChunk()
|
|
{
|
|
PngChunk chunk = new PngChunk();
|
|
|
|
if (ReadChunkLength(chunk) == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
byte[] typeBuffer = ReadChunkType(chunk);
|
|
|
|
ReadChunkData(chunk);
|
|
ReadChunkCrc(chunk, typeBuffer);
|
|
|
|
return chunk;
|
|
}
|
|
|
|
private void ReadChunkCrc(PngChunk chunk, byte[] typeBuffer)
|
|
{
|
|
// byte[] crcBuffer = new byte[4];
|
|
|
|
_stream.Position += 4;
|
|
// if (numBytes >= 1 && numBytes <= 3)
|
|
// {
|
|
// throw new Exception("Image stream is not valid!");
|
|
// }
|
|
|
|
// Array.Reverse(crcBuffer);
|
|
|
|
// chunk.Crc = BitConverter.ToUInt32(crcBuffer, 0);
|
|
|
|
// Crc32 crc = new Crc32();
|
|
// crc.Update(typeBuffer);
|
|
// crc.Update(chunk.Data);
|
|
|
|
// if (crc.Value != chunk.Crc)
|
|
// {
|
|
// throw new Exception("CRC Error. PNG Image chunk is corrupt!");
|
|
// }
|
|
}
|
|
|
|
private void ReadChunkData(PngChunk chunk)
|
|
{
|
|
chunk.Data = new byte[chunk.Length];
|
|
|
|
_stream.Read(chunk.Data, 0, chunk.Length);
|
|
}
|
|
|
|
private byte[] ReadChunkType(PngChunk chunk)
|
|
{
|
|
byte[] typeBuffer = new byte[4];
|
|
|
|
int numBytes = _stream.Read(typeBuffer, 0, 4);
|
|
if (numBytes >= 1 && numBytes <= 3)
|
|
{
|
|
throw new Exception("Image stream is not valid!");
|
|
}
|
|
|
|
char[] chars = new char[4];
|
|
chars[0] = (char)typeBuffer[0];
|
|
chars[1] = (char)typeBuffer[1];
|
|
chars[2] = (char)typeBuffer[2];
|
|
chars[3] = (char)typeBuffer[3];
|
|
|
|
chunk.Type = new string(chars);
|
|
|
|
return typeBuffer;
|
|
}
|
|
|
|
private int ReadChunkLength(PngChunk chunk)
|
|
{
|
|
byte[] lengthBuffer = new byte[4];
|
|
|
|
int numBytes = _stream.Read(lengthBuffer, 0, 4);
|
|
if (numBytes >= 1 && numBytes <= 3)
|
|
{
|
|
throw new Exception("Image stream is not valid!");
|
|
}
|
|
|
|
Array.Reverse(lengthBuffer);
|
|
|
|
chunk.Length = BitConverter.ToInt32(lengthBuffer, 0);
|
|
|
|
return numBytes;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
#endregion
|
|
|
|
#region ColorReaders
|
|
|
|
#region ColorReader
|
|
/// <summary>
|
|
/// Interface for color readers, which are responsible for reading
|
|
/// different color formats from a png file.
|
|
/// </summary>
|
|
private abstract class ColorReader
|
|
{
|
|
protected byte[] ToArrayByBitsLength(byte[] bytes, int bits)
|
|
{
|
|
byte[] result = null;
|
|
if (bits < 8)
|
|
{
|
|
result = new byte[bytes.Length * 8 / bits];
|
|
int factor = (int)Math.Pow(2, bits) - 1;
|
|
int mask = (0xFF >> (8 - bits));
|
|
int resultOffset = 0;
|
|
for (int i = 0; i < bytes.Length; i++)
|
|
{
|
|
for (int shift = 0; shift < 8; shift += bits)
|
|
{
|
|
int colorIndex = (((bytes[i]) >> (8 - bits - shift)) & mask) * (255 / factor);
|
|
result[resultOffset] = (byte)colorIndex;
|
|
resultOffset++;
|
|
}
|
|
}
|
|
}
|
|
else { result = bytes; }
|
|
return result;
|
|
}
|
|
|
|
public abstract void ReadScanline(byte[] scanline, byte[] pixels, PngHeader header);
|
|
}
|
|
#endregion
|
|
|
|
#region GrayscaleReader
|
|
/// <summary>
|
|
/// Color reader for reading grayscale colors from a PNG file.
|
|
/// </summary>
|
|
private sealed class GrayscaleReader : ColorReader
|
|
{
|
|
private int _row;
|
|
private bool _useAlpha;
|
|
public GrayscaleReader(bool useAlpha)
|
|
{
|
|
_useAlpha = useAlpha;
|
|
}
|
|
public override void ReadScanline(byte[] scanline, byte[] pixels, PngHeader header)
|
|
{
|
|
int offset = 0;
|
|
byte[] newScanline = ToArrayByBitsLength(scanline, header.BitDepth);
|
|
if (_useAlpha)
|
|
{
|
|
for (int x = 0; x < header.Width / 2; x++)
|
|
{
|
|
offset = (_row * header.Width + x) * 4;
|
|
pixels[offset + 0] = newScanline[x * 2];
|
|
pixels[offset + 1] = newScanline[x * 2];
|
|
pixels[offset + 2] = newScanline[x * 2];
|
|
pixels[offset + 3] = newScanline[x * 2 + 1];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int x = 0; x < header.Width; x++)
|
|
{
|
|
offset = (_row * header.Width + x) * 4;
|
|
pixels[offset + 0] = newScanline[x];
|
|
pixels[offset + 1] = newScanline[x];
|
|
pixels[offset + 2] = newScanline[x];
|
|
pixels[offset + 3] = (byte)255;
|
|
}
|
|
}
|
|
_row++;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region PaletteIndexReader
|
|
/// <summary>
|
|
/// A color reader for reading palette indices from the PNG file.
|
|
/// </summary>
|
|
private sealed class PaletteIndexReader : ColorReader
|
|
{
|
|
private int _row;
|
|
private byte[] _palette;
|
|
private byte[] _paletteAlpha;
|
|
public PaletteIndexReader(byte[] palette, byte[] paletteAlpha)
|
|
{
|
|
_palette = palette;
|
|
_paletteAlpha = paletteAlpha;
|
|
}
|
|
public override void ReadScanline(byte[] scanline, byte[] pixels, PngHeader header)
|
|
{
|
|
byte[] newScanline = ToArrayByBitsLength(scanline, header.BitDepth);
|
|
|
|
int offset = 0, index = 0;
|
|
|
|
if (_paletteAlpha != null && _paletteAlpha.Length > 0)
|
|
{
|
|
for (int i = 0; i < header.Width; i++)
|
|
{
|
|
index = newScanline[i];
|
|
offset = (_row * header.Width + i) * 4;
|
|
pixels[offset + 0] = _palette[index * 3];
|
|
pixels[offset + 1] = _palette[index * 3 + 1];
|
|
pixels[offset + 2] = _palette[index * 3 + 2];
|
|
pixels[offset + 3] = _paletteAlpha.Length > index ? _paletteAlpha[index] : (byte)255;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < header.Width; i++)
|
|
{
|
|
index = newScanline[i];
|
|
offset = (_row * header.Width + i) * 4;
|
|
pixels[offset + 0] = _palette[index * 3];
|
|
pixels[offset + 1] = _palette[index * 3 + 1];
|
|
pixels[offset + 2] = _palette[index * 3 + 2];
|
|
pixels[offset + 3] = (byte)255;
|
|
}
|
|
}
|
|
_row++;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region TrueColorReader
|
|
/// <summary>
|
|
/// Color reader for reading truecolors from a PNG file. Only colors
|
|
/// with 24 or 32 bit (3 or 4 bytes) per pixel are supported at the moment.
|
|
/// </summary>
|
|
sealed class TrueColorReader : ColorReader
|
|
{
|
|
private int _row;
|
|
private bool _useAlpha;
|
|
public TrueColorReader(bool useAlpha)
|
|
{
|
|
_useAlpha = useAlpha;
|
|
}
|
|
public override void ReadScanline(byte[] scanline, byte[] pixels, PngHeader header)
|
|
{
|
|
int offset = 0;
|
|
byte[] newScanline = ToArrayByBitsLength(scanline, header.BitDepth);
|
|
if (_useAlpha)
|
|
{
|
|
Array.Copy(newScanline, 0, pixels, _row * header.Width * 4, newScanline.Length);
|
|
}
|
|
else
|
|
{
|
|
for (int x = 0; x < newScanline.Length / 3; x++)
|
|
{
|
|
offset = (_row * header.Width + x) * 4;
|
|
pixels[offset + 0] = newScanline[x * 3];
|
|
pixels[offset + 1] = newScanline[x * 3 + 1];
|
|
pixels[offset + 2] = newScanline[x * 3 + 2];
|
|
pixels[offset + 3] = (byte)255;
|
|
}
|
|
}
|
|
_row++;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
#region PngChunkTypes
|
|
/// <summary>
|
|
/// Contains a list of possible chunk type identifier.
|
|
/// </summary>
|
|
private static class PngChunkTypes
|
|
{
|
|
/// <summary>
|
|
/// The first chunk in a png file. Can only exists once. Contains
|
|
/// common information like the width and the height of the image or
|
|
/// the used compression method.
|
|
/// </summary>
|
|
public const string Header = "IHDR";
|
|
/// <summary>
|
|
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
|
|
/// series in the RGB format.
|
|
/// </summary>
|
|
public const string Palette = "PLTE";
|
|
/// <summary>
|
|
/// The IDAT chunk contains the actual image data. The image can contains more
|
|
/// than one chunk of this type. All chunks together are the whole image.
|
|
/// </summary>
|
|
public const string Data = "IDAT";
|
|
/// <summary>
|
|
/// This chunk must appear last. It marks the end of the PNG datastream.
|
|
/// The chunk's data field is empty.
|
|
/// </summary>
|
|
public const string End = "IEND";
|
|
/// <summary>
|
|
/// This chunk specifies that the image uses simple transparency:
|
|
/// either alpha values associated with palette entries (for indexed-color images)
|
|
/// or a single transparent color (for grayscale and truecolor images).
|
|
/// </summary>
|
|
public const string PaletteAlpha = "tRNS";
|
|
/// <summary>
|
|
/// Textual information that the encoder wishes to record with the image can be stored in
|
|
/// tEXt chunks. Each tEXt chunk contains a keyword and a text string.
|
|
/// </summary>
|
|
public const string Text = "tEXt";
|
|
/// <summary>
|
|
/// This chunk specifies the relationship between the image samples and the desired
|
|
/// display output intensity.
|
|
/// </summary>
|
|
public const string Gamma = "gAMA";
|
|
/// <summary>
|
|
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
|
|
/// </summary>
|
|
public const string Physical = "pHYs";
|
|
}
|
|
#endregion
|
|
|
|
#region PngChunk
|
|
/// <summary>
|
|
/// Stores header information about a chunk.
|
|
/// </summary>
|
|
private sealed class PngChunk
|
|
{
|
|
/// <summary>
|
|
/// An unsigned integer giving the number of bytes in the chunk's
|
|
/// data field. The length counts only the data field, not itself,
|
|
/// the chunk type code, or the CRC. Zero is a valid length
|
|
/// </summary>
|
|
public int Length;
|
|
/// <summary>
|
|
/// A chunk type as string with 4 chars.
|
|
/// </summary>
|
|
public string Type;
|
|
/// <summary>
|
|
/// The data bytes appropriate to the chunk type, if any.
|
|
/// This field can be of zero length.
|
|
/// </summary>
|
|
public byte[] Data;
|
|
///// <summary>
|
|
///// A CRC (Cyclic Redundancy Check) calculated on the preceding bytes in the chunk,
|
|
///// including the chunk type code and chunk data fields, but not including the length field.
|
|
///// The CRC is always present, even for chunks containing no data
|
|
///// </summary>
|
|
//public uint Crc;
|
|
}
|
|
#endregion
|
|
|
|
#region PngColorTypeInformation
|
|
/// <summary>
|
|
/// Contains information that are required when loading a png with a specific color type.
|
|
/// </summary>
|
|
private sealed class PngColorTypeInformation
|
|
{
|
|
/// <summary>
|
|
/// Gets an array with the bit depths that are supported for the color type
|
|
/// where this object is created for.
|
|
/// </summary>
|
|
/// <value>The supported bit depths that can be used in combination with this color type.</value>
|
|
public int[] SupportedBitDepths { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets a function that is used the create the color reader for the color type where
|
|
/// this object is created for.
|
|
/// </summary>
|
|
/// <value>The factory function to create the color type.</value>
|
|
public Func<byte[], byte[], ColorReader> ScanlineReaderFactory { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets a factor that is used when iterating through the scanlines.
|
|
/// </summary>
|
|
/// <value>The scanline factor.</value>
|
|
public int ChannelsPerColor { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PngColorTypeInformation"/> class with
|
|
/// the scanline factory, the function to create the color reader and the supported bit depths.
|
|
/// </summary>
|
|
/// <param name="scanlineFactor">The scanline factor.</param>
|
|
/// <param name="supportedBitDepths">The supported bit depths.</param>
|
|
/// <param name="scanlineReaderFactory">The factory to create the color reader.</param>
|
|
public PngColorTypeInformation(int scanlineFactor, int[] supportedBitDepths, Func<byte[], byte[], ColorReader> scanlineReaderFactory)
|
|
{
|
|
ChannelsPerColor = scanlineFactor;
|
|
ScanlineReaderFactory = scanlineReaderFactory;
|
|
|
|
SupportedBitDepths = supportedBitDepths;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the color reader for the color type where this object is create for.
|
|
/// </summary>
|
|
/// <param name="palette">The palette of the image. Can be null when no palette is used.</param>
|
|
/// <param name="paletteAlpha">The alpha palette of the image. Can be null when
|
|
/// no palette is used for the image or when the image has no alpha.</param>
|
|
/// <returns>The color reader for the image.</returns>
|
|
public ColorReader CreateColorReader(byte[] palette, byte[] paletteAlpha)
|
|
{
|
|
return ScanlineReaderFactory(palette, paletteAlpha);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region PngHeader
|
|
/// <summary>
|
|
/// Represents the png header chunk.
|
|
/// </summary>
|
|
private sealed class PngHeader
|
|
{
|
|
/// <summary>
|
|
/// The dimension in x-direction of the image in pixels.
|
|
/// </summary>
|
|
public int Width;
|
|
/// <summary>
|
|
/// The dimension in y-direction of the image in pixels.
|
|
/// </summary>
|
|
public int Height;
|
|
/// <summary>
|
|
/// Bit depth is a single-byte integer giving the number of bits per sample
|
|
/// or per palette index (not per pixel). Valid values are 1, 2, 4, 8, and 16,
|
|
/// although not all values are allowed for all color types.
|
|
/// </summary>
|
|
public byte BitDepth;
|
|
/// <summary>
|
|
/// Color type is a integer that describes the interpretation of the
|
|
/// image data. Color type codes represent sums of the following values:
|
|
/// 1 (palette used), 2 (color used), and 4 (alpha channel used).
|
|
/// </summary>
|
|
public byte ColorType;
|
|
/// <summary>
|
|
/// Indicates the method used to compress the image data. At present,
|
|
/// only compression method 0 (deflate/inflate compression with a sliding
|
|
/// window of at most 32768 bytes) is defined.
|
|
/// </summary>
|
|
public byte CompressionMethod;
|
|
/// <summary>
|
|
/// Indicates the preprocessing method applied to the image
|
|
/// data before compression. At present, only filter method 0
|
|
/// (adaptive filtering with five basic filter types) is defined.
|
|
/// </summary>
|
|
public byte FilterMethod;
|
|
/// <summary>
|
|
/// Indicates the transmission order of the image data.
|
|
/// Two values are currently defined: 0 (no interlace) or 1 (Adam7 interlace).
|
|
/// </summary>
|
|
public byte InterlaceMethod;
|
|
}
|
|
#endregion
|
|
|
|
}
|
|
}
|