The AnimatedGif ClassThe AnimatedGif class has two methods that are called and several properties that can be set. This section explains how the code works. In essence, though, the calling code simply adds Bitmap objects to an instantiated class using the AddFrame() method, and then creates the animated GIF file by calling the CreateAnimation() method. The AnimatedGif class is used in a demo program named Dynamic BannerCS (and a VB version named DynamicBannerVB). This code can be seen in the section titled "Using the AnimatedGif Class." StructuresTo simplify the gathering and grouping of information, several structures are used. One structure contains the GIF header information, one the local image description, and one a graphics-control extension block. These structures are described in Table 11.4
Listing 11.1 contains the struct definitions that are used in the AnimagedGif class. Listing 11.1 The AnimatedGif Structuresstruct GifHeader { private byte[] _Signature; public byte[] Signature { get { return( _Signature ); } set { if( value.Length == 3 ) { _Signature = value; } } } private byte[] _Version; public byte[] Version { get { return( _Version ); } set { if( value.Length == 3 ) { _Version = value; } } } public short nScreenWidth, nScreenHeight; public byte Packed; public byte BackgroundColor; public byte AspectRatio; public int nSizeOfGCT; public int nColorTableEntries; public bool bColorTableSortFlag; public int nColorResolution; public bool bGlobalColorTableFlag; public byte[] GCT; }; struct GraphicsControlExtension { public byte Introducer; public byte Label; public byte BlockSize; public byte Packed; public short nDelayTime; public byte ColorIndex; public byte Terminator; }; struct LocalImageDescriptor { public byte Separator; public short nLeft, nTop, nWidth, nHeight; public byte Packed; public bool bLocalColorTableFlag; public bool bInterlaceFlag; public bool bSortFlag; public int nSizeLCT; public int nColorTableEntries; public byte[] LCT; }; Member VariablesThe AnimatedGif class doesn't have that many member variables. This limited number works because two of the member variables are structs that themselves contain a number of variables describing the image. These member variables can be seen along with their descriptions in Table 11.5.
The member variable declarations as found in the AnimatedGif class can be seen in Listing 11.2. Listing 11.2 The Member Variablesstring m_strFilenameFragment; int m_nFrameCount = 0; int m_nTransparentColorIndex = -1; GifHeader m_gh = new GifHeader(); LocalImageDescriptor m_ld = new LocalImageDescriptor(); int m_nDelayValue = 50; An Overview of the MethodsTable 11.6 contains a handy reference to the methods found both in the AnimatedGif class and the demo program that uses the class.
Reading and Writing the GIF HeaderReading the GIF header data is simple. Using the ReadBytes() method of the BinaryReader, the signature, version, width, height, flag byte (named Packed), background color, and aspect ratio are all read in. Once these values are read in, the code goes on to break the Packed variable apart. The lower 3 bits contain the size of the global color table, which can later be used to calculate the exact number of bytes in the global color table with this formula: gh.nColorTableEntries = ( 1 << ( gh.nSizeOfGCT + 1 ) ); If a global color table exists, it is read in the ReadHeader() method. The WriteHeader() method is simpler than the ReadHeader() method because it doesn't have to perform any calculations; it simply writes out the data using the Write() method. The ReadHeader() and WriteHeader() methods can be seen in Listing 11.3. Listing 11.3 Reading and Writing the GIF Headerprivate void ReadHeader( BinaryReader reader, ref GifHeader gh, ref int nFileBytes ) { gh.Signature = reader.ReadBytes( 3 ); gh.Version = reader.ReadBytes( 3 ); gh.nScreenWidth = reader.ReadInt16(); gh.nScreenHeight = reader.ReadInt16(); gh.Packed = reader.ReadByte(); gh.BackgroundColor = reader.ReadByte(); gh.AspectRatio = reader.ReadByte(); nFileBytes -= 13; gh.nSizeOfGCT = (int) ( gh.Packed & 0x07 ); gh.nColorTableEntries = 0; gh.bColorTableSortFlag = (bool) ( ( gh.Packed & 0x8 ) != 0 ); gh.nColorResolution = (int) ( ( gh.Packed & 0x70 ) >> 4 ); gh.bGlobalColorTableFlag = (bool) ( ( gh.Packed & 0x80 ) != 0 ); if( gh.bGlobalColorTableFlag ) { gh.nColorTableEntries = ( 1 << ( gh.nSizeOfGCT + 1 ) ); gh.GCT = reader.ReadBytes( gh.nColorTableEntries * 3 ); nFileBytes -= ( gh.nColorTableEntries * 3 ); } } private void WriteHeader( BinaryWriter writer, GifHeader gh ) { writer.Write( gh.Signature ); writer.Write( gh.Version ); writer.Write( gh.nScreenWidth ); writer.Write( gh.nScreenHeight); writer.Write( gh.Packed ); writer.Write( gh.BackgroundColor ); writer.Write( gh.AspectRatio ); if( gh.bGlobalColorTableFlag ) { writer.Write( gh.GCT ); } } Reading and Writing the Local Image DescriptorReading a Local Image Descriptor is easy, but just as with the GIF header, the ReadLocalImageDescriptor() method performs some calculations. Besides assigning values to some Boolean values, it calculates the size of the color table toward the end of the ReadLocalImageDescriptor() method. The local palette is also read in if it exits. Because it makes no calculations but simply writes data to disk, the WriteLocalImageDescriptor() method is simpler than the ReadImageDescriptor() Method. Both of these methods can be seen in Listing 11.4. Listing 11.4 Reading and Writing the Local Image Descriptorprivate void ReadLocalImageDescriptor( BinaryReader reader, ref LocalImageDescriptor ld, ref int nFileBytes ) { ld.Separator = reader.ReadByte(); ld.nLeft = reader.ReadInt16(); ld.nTop = reader.ReadInt16(); ld.nWidth = reader.ReadInt16(); ld.nHeight = reader.ReadInt16(); ld.Packed = reader.ReadByte(); nFileBytes -= 10; ld.bLocalColorTableFlag = (bool) ( ( ld.Packed & 0x01 ) != 0 ); ld.bInterlaceFlag = (bool) ( ( ld.Packed & 0x02 ) != 0 ); ld.bSortFlag = (bool) ( ( ld.Packed & 0x04 ) != 0 ); ld.nSizeLCT = (int) ( ( ld.Packed & 0xe ) >> 5 ); ld.nColorTableEntries = 0; if( ld.bLocalColorTableFlag ) { ld.nColorTableEntries = 1 << ( ld.nSizeLCT + 1 ); ld.LCT = reader.ReadBytes( ld.nColorTableEntries * 3 ); nFileBytes -= ( ld.nColorTableEntries * 3 ); } } private void WriteLocalImageDescriptor( BinaryWriter writer, LocalImageDescriptor ld ) { writer.Write( ld.Separator ); writer.Write( ld.nLeft ); writer.Write( ld.nTop ); writer.Write( ld.nWidth ); writer.Write( ld.nHeight ); writer.Write( ld.Packed ); if( ld.bLocalColorTableFlag ) { writer.Write( ld.LCT ); } } Writing the Graphics Control Extension and the Loop ControlFor the AnimatedGif class, the Graphics Control Extension indicates a delay value for the animation and a transparent color if the calling code set one. You can see the WriteGraphicsControlExtension() method in Listing 11.5. It simply writes the values for the GraphicsControlExtension structure to disk. Listing 11.5 Writing the Graphics Control Extensionprivate void WriteGraphicsControlExtension( BinaryWriter writer, GraphicsControlExtension gce ) { writer.Write( gce.Introducer ); writer.Write( gce.Label ); writer.Write( gce.BlockSize ); writer.Write( gce.Packed ); writer.Write( gce.nDelayTime ); writer.Write( gce.ColorIndex ); writer.Write( gce.Terminator ); } By default, GIF animations stop animating once they reach the last frame. A special control block must be written to the file for the animations to continuously restart at the beginning. Listing 11.6 shows the looping control block being written to disk. I got this information from www.Netscape.com because it was nowhere to be found on the Microsoft Web site. Listing 11.6 Writing the Loop Controlprivate void WriteLoopControl( BinaryWriter writer ) { byte[] bc = new byte[] { 0x21, 0xff, 0x0b, (byte)'N', (byte)'E', (byte)'T', (byte)'S', (byte)'C', (byte)'A', (byte)'P', (byte)'E', (byte)'2', (byte)'.', (byte)'0', 3, 1, 0, 0, 0 }; writer.Write( bc ); } Adding FramesOnce an AnimatedGif class has been instantiated, any number of frames can be added. The frames are added by passing a Bitmap object to the AddFrame() method. The Bitmap is then saved as a GIF image that will eventually be combined with all the others into the single animated image. It's important to note that a directory must exist with the necessary permissions into which these images can be saved. For this reason, a path is passed into the AddFrame() method so that it will save to the correct directory. The complete file name, including the path, is formed as the first order of business in the AddFrame() method. The file name is composed of the save path, the file name fragment that was formed in the AnimatedGif constructor (which is actually based on the current clock ticks), the frame number, and the .gif extension. Next, the image is saved by calling the Bitmap's Save() method. The frame count contained in the m_nFrameCount variable is incremented so that the class knows how many frames it has, and so that it knows how to form the next file name. Later on, we'll talk about the properties that deal with the transparent color (or index) of the animation. But I need to point out that the SetTransparentColor() method requires that we have a color palette with which the specified color can be matched. For this reason, before we leave the AddFrame() method, we need to load the palette. This is done by calling the ReadHeader() and ReadLocalImageDescriptor() methods. Why call both methods? Because neither is guaranteed: you can have a global or a local palette. The code in Listing 11.7 starts off with the AnimatedGif() constructor. In this code you can see the file name fragment being formed based on the system clock ticks. The AddFrame() method can also be seen in this listing. Listing 11.7 The AnimatedGif() Constructor and the AddFrame() Methodpublic AnimatedGif() { m_strFilenameFragment = "AnimationFrames" + Convert.ToString( DateTime.Now.Ticks ); } public void AddFrame( Bitmap img, string strSavePath ) { string strFilename = strSavePath + "\\" + m_strFilenameFragment + Convert.ToString( m_nFrameCount ) + ".gif"; img.Save( strFilename, ImageFormat.Gif ); m_nFrameCount++; if( m_nFrameCount == 1 ) { FileStream fs = new FileStream( strFilename, FileMode.Open ); int nFileBytes = (int) fs.Length; BinaryReader reader = new BinaryReader( fs ); ReadHeader( reader, ref m_gh, ref nFileBytes ); byte[] bt = reader.ReadBytes( 8 ); nFileBytes -= 8; ReadLocalImageDescriptor( reader, ref m_ld, ref nFileBytes ); fs.Close(); } } Creating the AnimationThe animated GIF file is created by loading each GIF file that was saved in the AddFrame() method and saving it all to one big animated GIF file, as shown in Listing 11.8. I'll talk about a few other details in this section. First, the file name for the animated GIF file is formed. The name starts with the file name fragment and then concatenates the string "_Complete.gif". FileStream and BinaryWriter objects are instantiated so that the output file can be written. A flag indicating when the first frame has been written is set to False so that the code knows when the first frame is about to be written. This step is important because a Graphics Control Extension and a Loop Control must be written before any images are written. A for loop counts through each frame that was saved. A file name for the frame image is formed, and FileStream and BinaryReader objects are created with which the file data can be read. First, the GIF header is read. Of course, this was already done when the first frame was saved, but the data must be read anyway. Another 8 bytes are read because the Microsoft GIF images always contain a single Graphics Control Extension that the code simply discards. The LocalImageDescriptor is read from the source file, and then the image data is read. You might note that when the image data is read, a determination is made of whether or not the image currently being read is the last. If it is not, the code reads one less byte than is available because the last byte is a terminator character and indicates the end of image data. If we have this data in the buffer and write it out into the complete GIF animation file, the decoder will stop when it sees the terminator. For this reason, the code reads in (and subsequently writes out) only the terminator byte for the last image. Once the code is done with the file, the FileStream object is closed, and the file is deleted. You could actually change the code so that the files aren't deleted. This change would let you call the CreateAnimation() method more than once because the individual frame images would still exist. You'd want to delete these frames somehow, though, possibly in some sort of Finalize() method. If the image that was just read in is the first image, the GIF header must be written out to the destination file. The Loop Control is also written out because this is the proper place. A Graphics Control Extension is created for each image that's to be written. The Graphics Control Extension is written to the destination file by calling the WriteGraphicsControlExtension() method. The last two things that are done for each image are to write the LocalImageDescriptor and the image data. Finally, the FileStream object for the output file is closed. The string that contains the file name for the newly created animation image is returned so that the calling code knows where the file is. Note that the file returned does not contain the full path, but simply the file name itself. Listing 11.8 Creating the Animationpublic string CreateAnimation( string strSavePath ) { // Create the destination file string. string strFilename = m_strFilenameFragment + "_Complete.gif"; // Create the file and get a writer from it. FileStream fs = new FileStream( strSavePath + "\\" + strFilename, FileMode.CreateNew ); BinaryWriter writer = new BinaryWriter( fs ); bool bWriteFirst = false; // Loop through each frame. for( int i=0; i<m_nFrameCount; i++ ) { // Create the string that contains the file for this // frame. string strSaveFile = strSavePath + "\\" + m_strFilenameFragment + Convert.ToString( i ) + ".gif"; // Open the file for reading and get a BinaryReader object. FileStream fs2 = new FileStream( strSaveFile, FileMode.Open ); int nFileBytes = (int) fs2.Length; BinaryReader reader = new BinaryReader( fs2 ); // Call the method that reads the GIF header. ReadHeader( reader, ref m_gh, ref nFileBytes ); // Read the Graphics Control Block, but we don't use it. byte[] bt = reader.ReadBytes( 8 ); nFileBytes -= 8; // Call the method that reads the Local Image Descriptor. ReadLocalImageDescriptor( reader, ref m_ld, ref nFileBytes ); // Unless this is the last frame, we'll omit the // separator byte. if( i < m_nFrameCount - 1 ) { nFileBytes--; } // Read the image data. byte[] ImageData = reader.ReadBytes( nFileBytes ); // Close this file and delete it. fs2.Close(); File.Delete( strSaveFile ); // We have to write the header for the first frame. if( !bWriteFirst ) { bWriteFirst = true; WriteHeader( writer, m_gh ); WriteLoopControl( writer ); } // Here we create the Graphics Control Extension // indicating the delay value for this frame. GraphicsControlExtension gce = new GraphicsControlExtension(); gce.Introducer = 0x21; gce.Label = 0xf9; gce.BlockSize = 4; gce.Packed = 8; gce.nDelayTime = (byte) m_nDelayValue; gce.ColorIndex = 0; gce.Terminator = 0; if( m_nTransparentColorIndex >= 0 ) { gce.ColorIndex = (byte) m_nTransparentColorIndex; gce.Packed = 9; } // Write the Graphics Control Extension and the // Local Image Descriptor. WriteGraphicsControlExtension( writer, gce ); WriteLocalImageDescriptor( writer, m_ld ); // Write the image data. writer.Write( ImageData ); } // Close the output file. fs.Close(); // Return the file name. return( strFilename ); } The PropertiesTwo properties (TransparentColorIndex and DelayValue) can be set, and also one helper method that makes it easy to set the transparent color index by passing a Color value. One thing to note, though, is that the SetTransparentColor() method finds the first RGB match for the specified color. If more than one palette entry contains the same RGB values, there is no guarantee that the transparent index will be correct. Listing 11.9 The AnimatedGif Propertiespublic int TransparentColorIndex { get { return( m_nTransparentColorIndex ); } set { m_nTransparentColorIndex = TransparentColorIndex; } } public void SetTransparentColor(Color col) { for( int i=0; i<m_gh.nColorTableEntries; i++ ) { if( col.R == m_gh.GCT[i*3] && col.G == m_gh.GCT[i*3+1] && col.B == m_gh.GCT[i*3+2] ) { m_nTransparentColorIndex = i; break; } } } public int DelayValue { get { return( m_nDelayValue ); } set { m_nDelayValue = DelayValue; } } |