//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 { /// /// Attempts to read count bytes of data from the supplied stream. /// /// The stream to read from /// The number of bytes to read /// A byte[] containing the data or null if an error occurred 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; } /// /// 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. /// /// An array of bytes from which the value is to be extracted /// The number of bytes to parse (must be <= sizeof(uint)) /// The extracted unsigned integer returned in a uint public static uint ParseUint(byte[] buffer, int uintLengthInBytes) { int offset = 0; return ParseUint(buffer, uintLengthInBytes, ref offset); } /// /// 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. /// /// An array of bytes from which the value is to be extracted /// The number of bytes to parse (must be <= sizeof(uint)) /// The offset in the array of bytes where parsing shall begin /// The extracted unsigned integer returned in a uint 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 { /// /// The first 8 bytes of an APNG encoding /// static byte[] expectedSignature = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a }; /// /// The signature parsed from the input stream /// private byte[] signature; /// /// Default constructor /// public APNGHeader() { signature = null; } /// /// Attempts to read an APNG Header chunk from the supplied stream. /// /// The stream containing the APNG Header 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 { /// /// The ASCII name of the APNG chunk /// public const String NAME = "MEND"; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public MENDChunk(APNGChunk chunk) : base(chunk, NAME) { } } internal class TERMChunk : APNGChunk { /// /// The ASCII name of the APNG chunk /// public const String NAME = "TERM"; private uint terminationAction; private uint actionAfterTermination; private uint delay; private uint iterationMax; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public TERMChunk(APNGChunk chunk) : base(chunk, NAME) { } /// /// Extracts various fields specific to this chunk from the APNG's /// data field /// /// An array of bytes representing the APNG's data field 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 { /// /// The ASCII name of the APNG chunk /// public const String NAME = "bKGD"; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public BKGDChunk(APNGChunk chunk) : base(chunk, NAME) { } } internal class BACKChunk : APNGChunk { /// /// The ASCII name of the APNG chunk /// public const String NAME = "BACK"; private uint redBackground; private uint greenBackground; private uint blueBackground; private uint mandatoryBackground; private uint backgroundImageId; private uint backgroundTiling; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public BACKChunk(APNGChunk chunk) : base(chunk, NAME) { } /// /// Extracts various fields specific to this chunk from the APNG's /// data field /// /// An array of bytes representing the APNG's data field 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 { /// /// The ASCII name of the APNG chunk /// 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; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public IHDRChunk(APNGChunk chunk) : base(chunk, NAME) { } /// /// Extracts various fields specific to this chunk from the APNG's /// data field /// /// An array of bytes representing the APNG's data field 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 { /// /// The ASCII name of the APNG chunk /// public const String NAME = "fcTL"; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public fcTLChunk(APNGChunk chunk) : base(chunk, NAME) { } public bool IsEmpty() { return ChunkLength == 0; } } internal class IENDChunk : APNGChunk { /// /// The ASCII name of the APNG chunk /// public const String NAME = "IEND"; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public IENDChunk(APNGChunk chunk) : base(chunk, NAME) { } } internal class IDATChunk : APNGChunk { /// /// The ASCII name of the APNG chunk /// public const String NAME = "IDAT"; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public IDATChunk(APNGChunk chunk) : base(chunk, NAME) { } } internal class fdATChunk : APNGChunk { /// /// The ASCII name of the APNG chunk /// public const String NAME = "fdAT"; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public fdATChunk(APNGChunk chunk) : base(chunk, NAME) { } } internal class acTLChunk : APNGChunk { /// /// The ASCII name of the APNG chunk /// public const String NAME = "acTL"; /// /// The APNG frame width in pixels /// private uint frameWidth; /// /// The APNG frame height in pixels /// private uint frameHeight; ///// ///// The APNG frame rate ///// //private uint ticksPerSecond; ///// ///// The APNG layer count ///// //private uint nominalLayerCount; ///// ///// The APNG frame count ///// //private uint nominalFrameCount; ///// ///// The APNG play time ///// //private uint nominalPlayTime; ///// ///// The APNG simplicity profile ///// //private uint simplicityProfile; /// /// Constructor /// /// The APNG chunk containing the data for this specific chunk public acTLChunk(APNGChunk chunk) : base(chunk, NAME) { } /// /// The APNG width in pixels /// public uint FrameWidth { get { return frameWidth; } } /// /// The APNG height in pixels /// public uint FrameHeight { get { return frameHeight; } } /// /// Extracts various fields specific to this chunk from the APNG's /// data field /// /// An array of bytes representing the APNG's data field 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; /// /// Default constructor /// public APNGChunk() { chunkLength = chunkType = chunkData = chunkCRC = null; error = "No Error"; } /// /// Constructor which takes an existing APNGChunk object and /// verifies that its type matches that which is expected /// /// The APNGChunk to copy /// The input APNGChunk expected type 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); } /// /// Extracts various fields specific to this chunk from the APNG's /// data field /// /// An array of bytes representing the APNG's data field protected virtual void ParseData(byte[] chunkData) { // Nothing specific to do here. Derived classes can override this // to do specific field parsing. } /// /// 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 /// 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; } } /// /// Gets the array of bytes which make up the chunk's data field /// public byte[] ChunkData { get { return chunkData; } } /// /// Gets chunk's type field as an string /// 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; } } /// /// Gets the length field of the chunk /// public uint ChunkLength { get { return APNGUtility.ParseUint(chunkLength, chunkLength.Length); } } /// /// Gets the CRC field of the chunk /// public uint ChunkCRC { get { return APNGUtility.ParseUint(chunkCRC, chunkCRC.Length); } } /// /// Attempts to parse an APNGChunk for the specified stream /// /// The stream containing the APNG Chunk 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 { /// /// The PNG file signature /// private static byte[] header = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; /// /// The PNG file's IHDR chunk /// private IHDRChunk ihdr; /// /// The PNG file's PLTE chunk (optional) /// private fcTLChunk plte; /// /// The PNG file's IDAT chunks /// private List idats; /// /// The PNG file's IEND chunk /// private IENDChunk iend; /// /// Default constructor /// public PNG() { idats = new List(); } /// /// 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 /// /// MemoryStream 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(); } } /// /// Gets or Sets the PNG's IHDR chunk /// public IHDRChunk IHDR { get { return ihdr; } set { ihdr = value; } } /// /// Gets or Sets the PNG's PLTE chunk /// public fcTLChunk FCTL { get { return plte; } set { plte = value; } } /// /// Gets or Sets the PNG's IEND chunk /// public IENDChunk IEND { get { return iend; } set { iend = value; } } /// /// Gets the list of IDAT chunk's making up the PNG /// public List IDATS { get { return idats; } } /// /// Adds the assigned IDAT chunk to the end of the PNG's list of IDAT chunks /// public IDATChunk IDAT { set { idats.Add(value); } } } public class APNG { /// /// List of chunks in the APNG /// List chunks; /// /// List of PNGs embedded in the APNG /// List pngs; /// /// The APNG's MHDRChunk /// acTLChunk headerChunk; IHDRChunk ihdrChunk; /// /// Gets the number of embedded PNGs within the APNG /// public int NumEmbeddedPNG { get { return pngs.Count; } } public Bitmap this[int Index] { get { return ToBitmap(Index); } } /// /// Creates a Bitmap object containing the embedded PNG at the specified /// index in the APNG's list of embedded PNGs /// /// The embedded PNG index /// Bitmap 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 /// /// Creates a string containing the names of all the chunks in the APNG /// /// String public override string ToString() { StringBuilder sb = new StringBuilder(); foreach (APNGChunk chunk in chunks) sb.AppendLine(chunk.ChunkType); return sb.ToString(); } /// /// Attempts to load an APNG from the specified file name /// /// Name of the APNG file to load public void Load(string filename) { chunks = new List(); pngs = new List(); // 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); } } }