Switch-Toolbox/Switch_Toolbox_Library/FileFormats/APNG/APNG.cs

921 lines
30 KiB
C#

//http://en.wikipedia.org/wiki/APNG
//From https://www.codeproject.com/Articles/36179/APNG-Viewer
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Drawing;
using System.IO;
namespace Toolbox.Library
{
internal class APNGUtility
{
/// <summary>
/// Attempts to read count bytes of data from the supplied stream.
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <param name="count">The number of bytes to read</param>
/// <returns>A byte[] containing the data or null if an error occurred</returns>
public static byte[] ReadStream(Stream stream, uint count)
{
byte[] bytes = new byte[count];
try
{
stream.Read(bytes, 0, (int)count);
}
catch (IOException)
{
throw;
}
return bytes;
}
/// <summary>
/// Attempts to parse an unsigned integer value from the array of bytes
/// provided. The most significant byte of the unsigned integer is
/// parsed from the first element in the array.
/// </summary>
/// <param name="buffer">An array of bytes from which the value is to be extracted</param>
/// <param name="uintLengthInBytes">The number of bytes to parse (must be <= sizeof(uint))</param>
/// <returns>The extracted unsigned integer returned in a uint</returns>
public static uint ParseUint(byte[] buffer, int uintLengthInBytes)
{
int offset = 0;
return ParseUint(buffer, uintLengthInBytes, ref offset);
}
/// <summary>
/// Attempts to parse an unsigned integer value from the array of bytes
/// provided. The most significant byte of the unsigned integer is
/// parsed from the specified offset in the array.
/// </summary>
/// <param name="buffer">An array of bytes from which the value is to be extracted</param>
/// <param name="uintLengthInBytes">The number of bytes to parse (must be <= sizeof(uint))</param>
/// <param name="offset">The offset in the array of bytes where parsing shall begin</param>
/// <returns>The extracted unsigned integer returned in a uint</returns>
public static uint ParseUint(byte[] buffer, int uintLengthInBytes, ref int offset)
{
uint value = 0;
if (uintLengthInBytes > sizeof(uint))
throw new ArgumentException(
String.Format("Function can only be used to parse up to {0} bytes from the buffer",
sizeof(uint)));
if (buffer.Length - offset < uintLengthInBytes)
throw new ArgumentException(
String.Format("buffer is not long enough to extract {0} bytes at offset {1}",
sizeof(uint), offset));
int i, j;
for (i = offset + uintLengthInBytes - 1, j = 0; i >= offset; i--, j++)
value |= (uint)(buffer[i] << (8 * j));
offset += uintLengthInBytes;
return value;
}
}
internal class APNGHeader
{
/// <summary>
/// The first 8 bytes of an APNG encoding
/// </summary>
static byte[] expectedSignature = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
/// <summary>
/// The signature parsed from the input stream
/// </summary>
private byte[] signature;
/// <summary>
/// Default constructor
/// </summary>
public APNGHeader()
{
signature = null;
}
/// <summary>
/// Attempts to read an APNG Header chunk from the supplied stream.
/// </summary>
/// <param name="stream">The stream containing the APNG Header</param>
public void Read(Stream stream)
{
// Stream must be readable
if (!stream.CanRead)
throw new ArgumentException("Stream is not readable");
// Read the signature
try
{
signature = APNGUtility.ReadStream(stream, 8);
}
catch (Exception)
{
// Re-throw any exceptions
throw;
}
// Test signature for validity
if (signature.Length == expectedSignature.Length)
{
for (int i = 0; i < expectedSignature.Length; i++)
{
// Invalid signature
if (expectedSignature[i] != signature[i])
throw new ApplicationException("APNG signature not found.");
}
}
else
// Invalid signature
throw new ApplicationException("APNG signature not found.");
}
}
internal class MENDChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "MEND";
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public MENDChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
}
internal class TERMChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "TERM";
private uint terminationAction;
private uint actionAfterTermination;
private uint delay;
private uint iterationMax;
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public TERMChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
/// <summary>
/// Extracts various fields specific to this chunk from the APNG's
/// data field
/// </summary>
/// <param name="chunkData">An array of bytes representing the APNG's data field</param>
protected override void ParseData(byte[] chunkData)
{
int offset = 0;
terminationAction = APNGUtility.ParseUint(chunkData, 1, ref offset);
// If the data length is > 1 then read 9 more bytes
if (chunkData.Length > 1)
{
actionAfterTermination = APNGUtility.ParseUint(chunkData, 1, ref offset);
delay = APNGUtility.ParseUint(chunkData, 4, ref offset);
iterationMax = APNGUtility.ParseUint(chunkData, 4, ref offset);
}
}
}
internal class BKGDChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "bKGD";
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public BKGDChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
}
internal class BACKChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "BACK";
private uint redBackground;
private uint greenBackground;
private uint blueBackground;
private uint mandatoryBackground;
private uint backgroundImageId;
private uint backgroundTiling;
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public BACKChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
/// <summary>
/// Extracts various fields specific to this chunk from the APNG's
/// data field
/// </summary>
/// <param name="chunkData">An array of bytes representing the APNG's data field</param>
protected override void ParseData(byte[] chunkData)
{
int offset = 0;
redBackground = APNGUtility.ParseUint(chunkData, 2, ref offset);
greenBackground = APNGUtility.ParseUint(chunkData, 2, ref offset);
blueBackground = APNGUtility.ParseUint(chunkData, 2, ref offset);
// If the data length is > 6 then read 1 more byte
if (chunkData.Length > 6)
{
mandatoryBackground = APNGUtility.ParseUint(chunkData, 1, ref offset);
}
// If the data length is > 7 then read 2 more bytes
if (chunkData.Length > 7)
{
backgroundImageId = APNGUtility.ParseUint(chunkData, 2, ref offset);
}
// If the data length is > 9 then read 1 more byte
if (chunkData.Length > 9)
{
backgroundTiling = APNGUtility.ParseUint(chunkData, 1, ref offset);
}
}
}
internal class IHDRChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "IHDR";
private uint width;
private uint height;
private uint bitDepth;
private uint colorType;
private uint compressionMethod;
private uint filterMethod;
private uint interlaceMethod;
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public IHDRChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
/// <summary>
/// Extracts various fields specific to this chunk from the APNG's
/// data field
/// </summary>
/// <param name="chunkData">An array of bytes representing the APNG's data field</param>
protected override void ParseData(byte[] chunkData)
{
int offset = 0;
width = APNGUtility.ParseUint(chunkData, 4, ref offset);
height = APNGUtility.ParseUint(chunkData, 4, ref offset);
bitDepth = APNGUtility.ParseUint(chunkData, 1, ref offset);
colorType = APNGUtility.ParseUint(chunkData, 1, ref offset);
compressionMethod = APNGUtility.ParseUint(chunkData, 1, ref offset);
filterMethod = APNGUtility.ParseUint(chunkData, 1, ref offset);
interlaceMethod = APNGUtility.ParseUint(chunkData, 1, ref offset);
}
}
internal class fcTLChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "fcTL";
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public fcTLChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
public bool IsEmpty()
{
return ChunkLength == 0;
}
}
internal class IENDChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "IEND";
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public IENDChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
}
internal class IDATChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "IDAT";
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public IDATChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
}
internal class fdATChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "fdAT";
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public fdATChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
}
internal class acTLChunk : APNGChunk
{
/// <summary>
/// The ASCII name of the APNG chunk
/// </summary>
public const String NAME = "acTL";
/// <summary>
/// The APNG frame width in pixels
/// </summary>
private uint frameWidth;
/// <summary>
/// The APNG frame height in pixels
/// </summary>
private uint frameHeight;
///// <summary>
///// The APNG frame rate
///// </summary>
//private uint ticksPerSecond;
///// <summary>
///// The APNG layer count
///// </summary>
//private uint nominalLayerCount;
///// <summary>
///// The APNG frame count
///// </summary>
//private uint nominalFrameCount;
///// <summary>
///// The APNG play time
///// </summary>
//private uint nominalPlayTime;
///// <summary>
///// The APNG simplicity profile
///// </summary>
//private uint simplicityProfile;
/// <summary>
/// Constructor
/// </summary>
/// <param name="chunk">The APNG chunk containing the data for this specific chunk</param>
public acTLChunk(APNGChunk chunk)
: base(chunk, NAME)
{
}
/// <summary>
/// The APNG width in pixels
/// </summary>
public uint FrameWidth
{
get { return frameWidth; }
}
/// <summary>
/// The APNG height in pixels
/// </summary>
public uint FrameHeight
{
get { return frameHeight; }
}
/// <summary>
/// Extracts various fields specific to this chunk from the APNG's
/// data field
/// </summary>
/// <param name="chunkData">An array of bytes representing the APNG's data field</param>
protected override void ParseData(byte[] chunkData)
{
int offset = 0;
frameWidth = APNGUtility.ParseUint(chunkData, 4, ref offset);
frameHeight = APNGUtility.ParseUint(chunkData, 4, ref offset);
//ticksPerSecond = APNGUtility.ParseUint(chunkData, 4, ref offset);
//nominalLayerCount = APNGUtility.ParseUint(chunkData, 4, ref offset);
//nominalFrameCount = APNGUtility.ParseUint(chunkData, 4, ref offset);
//nominalPlayTime = APNGUtility.ParseUint(chunkData, 4, ref offset);
//simplicityProfile = APNGUtility.ParseUint(chunkData, 4, ref offset);
}
}
internal class APNGChunk
{
protected String error;
protected byte[] chunkLength;
protected byte[] chunkType;
protected byte[] chunkData;
protected byte[] chunkCRC;
protected uint calculatedCRC;
/// <summary>
/// Default constructor
/// </summary>
public APNGChunk()
{
chunkLength = chunkType = chunkData = chunkCRC = null;
error = "No Error";
}
/// <summary>
/// Constructor which takes an existing APNGChunk object and
/// verifies that its type matches that which is expected
/// </summary>
/// <param name="chunk">The APNGChunk to copy</param>
/// <param name="expectedType">The input APNGChunk expected type</param>
public APNGChunk(APNGChunk chunk, String expectedType)
{
// Copy the existing chunks members
chunkLength = chunk.chunkLength;
chunkType = chunk.chunkType;
chunkData = chunk.chunkData;
chunkCRC = chunk.chunkCRC;
// Verify the chunk type is as expected
if (ChunkType != expectedType)
throw new ArgumentException(
String.Format("Specified chunk type is not {0} as expected", expectedType));
// Parse the chunk's data
ParseData(chunkData);
}
/// <summary>
/// Extracts various fields specific to this chunk from the APNG's
/// data field
/// </summary>
/// <param name="chunkData">An array of bytes representing the APNG's data field</param>
protected virtual void ParseData(byte[] chunkData)
{
// Nothing specific to do here. Derived classes can override this
// to do specific field parsing.
}
/// <summary>
/// Gets the array of bytes which make up the APNG chunk. This includes:
/// o 4 bytes of the chunk's length
/// o 4 bytes of the chunk's type
/// o N bytes of the chunk's data
/// o 4 bytes of the chunk's CRC
/// </summary>
public byte[] Chunk
{
get
{
byte[] ba = new byte[chunkLength.Length +
chunkType.Length + chunkData.Length +
chunkCRC.Length];
chunkLength.CopyTo(ba, 0);
chunkType.CopyTo(ba, chunkLength.Length);
chunkData.CopyTo(ba, chunkLength.Length + chunkType.Length);
chunkCRC.CopyTo(ba, chunkLength.Length + chunkType.Length + chunkData.Length);
return ba;
}
}
/// <summary>
/// Gets the array of bytes which make up the chunk's data field
/// </summary>
public byte[] ChunkData
{
get
{
return chunkData;
}
}
/// <summary>
/// Gets chunk's type field as an string
/// </summary>
public String ChunkType
{
get
{
return new String(new char[] { (char)chunkType[0], (char)chunkType[1], (char)chunkType[2], (char)chunkType[3] });
}
set
{
chunkType = Encoding.ASCII.GetBytes(value);
byte[] newChunkData = new byte[chunkData.Length - 4];
Array.Copy(chunkData, 4, newChunkData, 0, newChunkData.Length);
chunkData = newChunkData;
chunkLength = BitConverter.GetBytes(chunkData.Length);
Array.Reverse(chunkLength);
uint crc = CRC.INITIAL_CRC;
crc = CRC.UpdateCRC(crc, chunkType);
crc = CRC.UpdateCRC(crc, chunkData);
// CRC is inverted
crc = ~crc;
byte[] array = BitConverter.GetBytes(crc);
Array.Reverse(array);
chunkCRC = array;
}
}
/// <summary>
/// Gets the length field of the chunk
/// </summary>
public uint ChunkLength
{
get
{
return APNGUtility.ParseUint(chunkLength, chunkLength.Length);
}
}
/// <summary>
/// Gets the CRC field of the chunk
/// </summary>
public uint ChunkCRC
{
get
{
return APNGUtility.ParseUint(chunkCRC, chunkCRC.Length);
}
}
/// <summary>
/// Attempts to parse an APNGChunk for the specified stream
/// </summary>
/// <param name="stream">The stream containing the APNG Chunk</param>
public void Read(Stream stream)
{
if (!stream.CanRead)
throw new ArgumentException("Stream is not readable");
calculatedCRC = CRC.INITIAL_CRC;
long chunkStart = stream.Position;
// Read the data Length
chunkLength = APNGUtility.ReadStream(stream, 4);
// Read the chunk type
chunkType = APNGUtility.ReadStream(stream, 4);
calculatedCRC = CRC.UpdateCRC(calculatedCRC, chunkType);
// Read the data
chunkData = APNGUtility.ReadStream(stream, ChunkLength);
calculatedCRC = CRC.UpdateCRC(calculatedCRC, chunkData);
// Read the CRC
chunkCRC = APNGUtility.ReadStream(stream, 4);
// CRC is inverted
calculatedCRC = ~calculatedCRC;
// Verify the CRC
if (ChunkCRC != calculatedCRC)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(String.Format("APNG Chunk CRC Mismatch. Chunk CRC = {0}, Calculated CRC = {1}.",
ChunkCRC, calculatedCRC));
sb.AppendLine(String.Format("This occurred while parsing the chunk at position {0} (0x{1:X8}) in the stream.",
chunkStart, chunkStart));
throw new ApplicationException(sb.ToString());
}
}
}
internal class PNG
{
/// <summary>
/// The PNG file signature
/// </summary>
private static byte[] header = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
/// <summary>
/// The PNG file's IHDR chunk
/// </summary>
private IHDRChunk ihdr;
/// <summary>
/// The PNG file's PLTE chunk (optional)
/// </summary>
private fcTLChunk plte;
/// <summary>
/// The PNG file's IDAT chunks
/// </summary>
private List<IDATChunk> idats;
/// <summary>
/// The PNG file's IEND chunk
/// </summary>
private IENDChunk iend;
/// <summary>
/// Default constructor
/// </summary>
public PNG()
{
idats = new List<IDATChunk>();
}
/// <summary>
/// Converts the chunks making up the PNG into a single MemoryStream which
/// is suitable for writing to a PNG file or creating a Image object using
/// Bitmap.FromStream
/// </summary>
/// <returns>MemoryStream</returns>
public MemoryStream ToStream()
{
MemoryStream ms = new MemoryStream();
ms.Write(header, 0, header.Length);
ms.Write(ihdr.Chunk, 0, ihdr.Chunk.Length);
foreach (IDATChunk chunk in idats)
ms.Write(chunk.Chunk, 0, chunk.Chunk.Length);
ms.Write(iend.Chunk, 0, iend.Chunk.Length);
return ms;
}
public void SaveFile(string FileName)
{
using (FileStream writer = new FileStream(FileName, FileMode.Create))
{
MemoryStream mem = ToStream();
mem.Position = 0;
byte[] content = new byte[mem.Length];
mem.Read(content, 0, content.Length);
writer.Write(content, 0, content.Length);
writer.Close();
}
}
/// <summary>
/// Gets or Sets the PNG's IHDR chunk
/// </summary>
public IHDRChunk IHDR
{
get { return ihdr; }
set { ihdr = value; }
}
/// <summary>
/// Gets or Sets the PNG's PLTE chunk
/// </summary>
public fcTLChunk FCTL
{
get { return plte; }
set { plte = value; }
}
/// <summary>
/// Gets or Sets the PNG's IEND chunk
/// </summary>
public IENDChunk IEND
{
get { return iend; }
set { iend = value; }
}
/// <summary>
/// Gets the list of IDAT chunk's making up the PNG
/// </summary>
public List<IDATChunk> IDATS
{
get { return idats; }
}
/// <summary>
/// Adds the assigned IDAT chunk to the end of the PNG's list of IDAT chunks
/// </summary>
public IDATChunk IDAT
{
set { idats.Add(value); }
}
}
public class APNG
{
/// <summary>
/// List of chunks in the APNG
/// </summary>
List<APNGChunk> chunks;
/// <summary>
/// List of PNGs embedded in the APNG
/// </summary>
List<PNG> pngs;
/// <summary>
/// The APNG's MHDRChunk
/// </summary>
acTLChunk headerChunk;
IHDRChunk ihdrChunk;
/// <summary>
/// Gets the number of embedded PNGs within the APNG
/// </summary>
public int NumEmbeddedPNG
{
get { return pngs.Count; }
}
public Bitmap this[int Index]
{
get
{
return ToBitmap(Index);
}
}
/// <summary>
/// Creates a Bitmap object containing the embedded PNG at the specified
/// index in the APNG's list of embedded PNGs
/// </summary>
/// <param name="index">The embedded PNG index</param>
/// <returns>Bitmap</returns>
public Bitmap ToBitmap(int index)
{
// Verify the index
if (index > NumEmbeddedPNG)
throw new ArgumentException(String.Format(
"Embedded PNG index must be between 0 and {0}", NumEmbeddedPNG - 1));
// Create the bitmap
Bitmap b = (Bitmap)Bitmap.FromStream(pngs[index].ToStream());
return b;
}
#if (DEBUG)
public void SaveFile(int index, string FileName)
{
// Verify the index
if (index > NumEmbeddedPNG)
throw new ArgumentException(String.Format(
"Embedded PNG index must be between 0 and {0}", NumEmbeddedPNG - 1));
pngs[index].SaveFile(FileName);
}
#endif
/// <summary>
/// Creates a string containing the names of all the chunks in the APNG
/// </summary>
/// <returns>String</returns>
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (APNGChunk chunk in chunks)
sb.AppendLine(chunk.ChunkType);
return sb.ToString();
}
/// <summary>
/// Attempts to load an APNG from the specified file name
/// </summary>
/// <param name="filename">Name of the APNG file to load</param>
public void Load(string filename)
{
chunks = new List<APNGChunk>();
pngs = new List<PNG>();
// Open the file for reading
Stream stream = File.OpenRead(filename);
// Create a new header (should be 1 per file) and
// read it from the stream
APNGHeader header = new APNGHeader();
try
{
header.Read(stream);
}
catch (Exception)
{
stream.Close();
throw;
}
APNGChunk chunk;
PNG png = null;
// Read chunks from the stream until we reach the MEND chunk
do
{
// Read a generic Chunk
chunk = new APNGChunk();
try
{
chunk.Read(stream);
}
catch (Exception)
{
stream.Close();
throw;
}
// Take a look at the chunk type and decide what derived class to
// use to create a specific chunk
switch (chunk.ChunkType)
{
case acTLChunk.NAME:
if (headerChunk != null)
throw new ApplicationException(String.Format(
"Only one chunk of type {0} is allowed", chunk.ChunkType));
chunk = headerChunk = new acTLChunk(chunk);
break;
case MENDChunk.NAME:
chunk = new MENDChunk(chunk);
break;
case TERMChunk.NAME:
chunk = new TERMChunk(chunk);
break;
case BACKChunk.NAME:
chunk = new BACKChunk(chunk);
break;
case BKGDChunk.NAME:
chunk = new BKGDChunk(chunk);
break;
case fcTLChunk.NAME:
// This is the beginning of a new embedded PNG
chunk = new fcTLChunk(chunk);
png = new PNG();
png.FCTL = chunk as fcTLChunk;
png.IHDR = ihdrChunk;
pngs.Add(png);
break;
case IHDRChunk.NAME:
chunk = new IHDRChunk(chunk);
ihdrChunk = chunk as IHDRChunk;
break;
case IDATChunk.NAME:
chunk = new IDATChunk(chunk);
if (png != null)
{
png.IDAT = chunk as IDATChunk;
}
break;
case fdATChunk.NAME:
chunk = new fdATChunk(chunk);
if (png != null)
{
chunk.ChunkType = IDATChunk.NAME;
png.IDAT = new IDATChunk(chunk);
}
break;
case IENDChunk.NAME:
chunk = new IENDChunk(chunk);
for (int i = 0; i < pngs.Count; i++)
{
pngs[i].IEND = chunk as IENDChunk;
}
break;
default:
break;
}
// Add the chunk to our list of chunks
chunks.Add(chunk);
}
while (chunk.ChunkType != IENDChunk.NAME);
}
}
}