It's almost the season for Christmas, and what a fun way to tie in the season with some programming than learning how to make .GIF files with C#. We are going to be making a Christmas tree as shown below!
Merry Christmas |
Christmas doesn't last for just one day, so let's prepare to celebrate it when it comes!
What's in a .GIF?
The .GIF file format is made up of many different parts, but this picture does the best job at explaining that at a glance.
The .GIF file format |
Each section is a collection of bytes written to a file in a particular format. By following the specification, we can write these bytes to a file and create for us our own .GIF file by hand.
No, you don't have to create .GIF files by hand. You can use different libraries to do the same thing we are about to do. In fact, if you are intending to use any .GIF - related code in production, you should be using a library. Libraries are better tested and have better performance.
How do we write bytes to a file?
At the core of creating a .GIF file from scratch is having the ability to write bytes to a file, if we didn't have this ability in C#, I wouldn't be able to write this article (so there is good news). If you are like most developers, you likely do not have to work so low-level (well - not that low level, but you get the picture) in dealing with bytes and files, but it is quite easy to do in C#.
In C#, we have streams which act as an abstraction over sending bytes to a data source. We will be using the FileStream in order to write the bytes to a file. A stream is not enough to write bytes to a file however, we need something else. We need a writer.
Another way to put this, is a stream creates for us a connection between our code and the file, and a writer actually sends the bytes to the file (using the stream). It's a two-player game, we need both players to succeed.
Quite a good two player game |
An example of using a stream, and a writer (we will be using BinaryWriter), is below.
using (FileStream fileStream = new FileStream(@"C:\folder\newfile.gif", FileMode.Create)) using (BinaryWriter output = new BinaryWriter(fileStream)) { output.Write(1); }In this example, we create a stream in a using statement (otherwise, we'd have to call the .Dispose method which is just an extra step we can avoid if we use using) passing in a file path and that we want to create or overwrite the file if it doesn't exist/exists. From that stream, we create a writer and then output a 1 to the file.
I actually prefer and recommend that you include curly brackets all the time. I just wanted to show you that some programmers do code double using statements without brackets around the outer using statement.
using (FileStream fileStream = new FileStream(@"C:\folder\newfile.gif", FileMode.Create)) { using (BinaryWriter output = new BinaryWriter(fileStream)) { output.Write(1); } }It is worth mentioning that by writing 1 to our file, we are writing 4 bytes to our file. How is that? By default, any integers in C# are actually 32 bit.
32 bit integer |
I found this out as I was writing this content, which tripped me up, and caused my .GIF images to not be properly encoded. We won't be using int a lot here, mainly using ushort and byte instead.
What bytes do we write to the file?
In order to keep this post concise, I'm not going to post all of the explanations why we are writing certain bytes when creating a .GIF file - but I will go into detail when necessary. We will be creating this .GIF first as an example.
The first set of bytes are the header. The header is 6 bytes long whose values are:
47 49 46 38 39 61When viewing the values in our .GIF file, including the header, we will see them in hexadecimal. It does not matter if we write these values as bytes or ushorts, we can still view the values as hexadecimal because we can convert between the values.
This value is decoded to "GIF89a" - which is the signature of the .GIF file "GIF", and the version of the .GIF specification "89a".
How are these values written in C#? Simply.
binaryWriter.Write(new char[3] { 'G', 'I', 'F' }); binaryWriter.Write(new char[3] { '8', '9', 'a' });The BinaryWriter can take in a character array, and each character gets converted into a byte. "G" becomes 47 in hexadecimal, "I" becomes 49, and so on.
Logical screen descriptor
The next set of bytes is the logical screen descriptor, and is 7 bytes long. Within the logical screen descriptor, we set 2 bytes for the width and 2 bytes for the height of the .GIF. The next byte is unique in that it is a packed byte, which contains information about the color resolution and the number of colors we have in our .GIF file (among other things). The last two bytes are the color of our background and our pixel aspect ratio. This image explains it best.The logical screen descriptor |
The maximum width and height of a .GIF is 65,535 pixels wide, as 65,535 is the maximum value of an ushort (which corresponds to the 2 bytes allocated for width and height respectively within the logical screen descriptor).
We are not worrying about background colors or the pixel aspect ratio, so those two bytes will stay 0. For the packed byte, we will want the same options as above. The only bytes that need to change are the .GIF's width and height, which are 2. Our logical screen descriptor for our example would look like this.
02 00 02 00 91 00 00How is this written in C#? Well, since the width and height are both 2 bytes long, we can write a ushort to our BinaryWriter (a ushort is 16 bits long). We also make use of hexadecimal literals in order to write out the hex directly in our code (we still have to convert it to a byte however, as hexadecimal is 32 bit).
binaryWriter.Write((ushort)2); binaryWriter.Write((ushort)2); binaryWriter.Write((byte)0x91); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0);
Global color table
In .GIF files, color tables hold all the colors that are contained in the .GIF. There is a concept of a global or local color table. If there is a local color table, it takes precedence over the global color table. If no local color table is present, the global color table is used. Since we set the global color table flag above (in the logical screen descriptor) to 1, we need a global color table.
What is the difference between a global and local color table? Why would we choose one or the other? A global color table is used for all of the frames of a .GIF, while a local color table is used for a single frame. You would pick between one or the other when you are concerned with optimal efficiency while encoding the .GIF - but for our use case we will just use the global color table.
The global color table is simply a list of RGB values [in hexadecimal] of all the colors in our .GIF. The red value is first, then the green value, followed by the blue value. For our sample, the global color table is this.
What is the difference between a global and local color table? Why would we choose one or the other? A global color table is used for all of the frames of a .GIF, while a local color table is used for a single frame. You would pick between one or the other when you are concerned with optimal efficiency while encoding the .GIF - but for our use case we will just use the global color table.
The global color table is simply a list of RGB values [in hexadecimal] of all the colors in our .GIF. The red value is first, then the green value, followed by the blue value. For our sample, the global color table is this.
ff ff ff 00 00 00 ff 00 00 00 ff 00White is the first color in our color table (R = ff, G = ff, B = ff), then followed by black (R = 00, G = 00, B = 00), followed by ted (R = ff, G = 00, B = 00), followed by green (R = 00, G = ff, B = 00).
This is written in C# like so.
binaryWriter.Write((byte)255); binaryWriter.Write((byte)255); binaryWriter.Write((byte)255); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); binaryWriter.Write((byte)255); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); binaryWriter.Write((byte)255); binaryWriter.Write((byte)0);
Application extension
.GIF's repeat, that's what makes them great. It is the application extension that allows our .GIFs to repeat. The application extension is mostly static*.
21 ff 0b 4e 45 54 53 43 41 50 45 32 2e 30 03 01 ff ff 00*The 2nd and 3rd byte from the end of the application extension tell the .GIF file how many times to repeat, so you could change this value if you'd like. The entire value is 16 bytes long (ff ff = 65,535).
The whole application extension value decoded is "! .NETSCAPE2.0.. ." The "NETSCAPE2.0" part identifies the application of the file and application authentication code (2.0). The browser itself is long gone, and was relevant when this specification was being made.
The application extension, and all other sections that follow, need to be written to the .GIF file for every frame in the .GIF file. Small, but important, note! Refer to the image up above for a visual.
This is C# code that will write the application extension out.
binaryWriter.Write((byte)0x21); binaryWriter.Write((byte)0xff); binaryWriter.Write((byte)0x0b); binaryWriter.Write('N'); binaryWriter.Write('E'); binaryWriter.Write('T'); binaryWriter.Write('S'); binaryWriter.Write('C'); binaryWriter.Write('A'); binaryWriter.Write('P'); binaryWriter.Write('E'); binaryWriter.Write('2'); binaryWriter.Write('.'); binaryWriter.Write('0'); binaryWriter.Write((byte)3); binaryWriter.Write((byte)1); binaryWriter.Write((byte)0xff); binaryWriter.Write((byte)0xff); binaryWriter.Write((byte)0);
Graphic control extension
The graphic control extension lets us control the animation speed of the .GIF, how new images are rendered between frames of .GIF files, and if any color should be transparent in the .GIF (among other things).
For our example, we are going to write each new image over the existing image in the animation, as well as delay the frame by 100 centiseconds. No color will be transparent in our .GIF.
Why centiseconds? I don't really know.
Writing out the graphic control extension would be the following values.
21 f9 04 04 64 00 00 00100 centiseconds in hexadecimal turns out to equal "64". Since the value for the frame delay takes up 2 bytes, the "64 00" part of the graphic control extension controls the delay (not just the "64")
This is how you would write the graphic control extension.
binaryWriter.Write((byte)0x21); binaryWriter.Write((byte)0xf9); binaryWriter.Write((byte)4); binaryWriter.Write((byte)4); binaryWriter.Write((ushort)100); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0);
Image descriptor
The image descriptor contains information of how large an image frame is, and what position it should start displaying at (you can think of these as an offset-x and offset-y positions), among other things. The entire image descriptor is 10 bytes long, and for our example, will look like this.
2c 00 00 00 00 02 00 02 00 00The 6th and 7th bytes are the frame's width, and the 8th and 9th are the frame's height. The 2nd and 3rd bytes are the offset-x, and the 4th and 5th bytes are the offset-y.
The image descriptor in C#.
binaryWriter.Write((byte)0x2c); binaryWriter.Write((ushort)0); binaryWriter.Write((ushort)0); binaryWriter.Write((ushort)2); binaryWriter.Write((ushort)2); binaryWriter.Write((byte)0);
Image data
The image data contains the pixels of the frame, encoded using LZW compression. I'm not going to go into detail about the implementation of LZW, as I'll share with you code you can look at if you want details at the end of this blog post.
The image data for our sample .GIF is.
02 03 c4 14 05 00
binaryWriter.Write((byte)2); binaryWriter.Write((byte)3); binaryWriter.Write((byte)0xc4); binaryWriter.Write((byte)0x14); binaryWriter.Write((byte)5); binaryWriter.Write((byte)0);
Aannndd..
The next image frame begins again with the graphic control extension, following the our existing sections we defined above. This cycle repeats for every frame we want to have in our .GIF file. Our sample only has one frame however, so we are not repeating any section again.
When writing all of the bytes out to file, this is what our .GIF's structure looks like.
When writing all of the bytes out to file, this is what our .GIF's structure looks like.
Our sample .GIF file |
Good job, you've just written your first .GIF file! The full code to create the sample .GIF is below.
public class Sample { public static void Main(string[] args) { using (FileStream fileStream = new FileStream(@"C:\folder\newfile.gif", FileMode.Create)) { using (BinaryWriter binaryWriter = new BinaryWriter(fileStream)) { // header binaryWriter.Write(new char[3] { 'G', 'I', 'F' }); binaryWriter.Write(new char[3] { '8', '9', 'a' }); // logical screen descriptor binaryWriter.Write((ushort)2); binaryWriter.Write((ushort)2); binaryWriter.Write((byte)0x91); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); // global color table binaryWriter.Write((byte)255); binaryWriter.Write((byte)255); binaryWriter.Write((byte)255); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); binaryWriter.Write((byte)255); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); binaryWriter.Write((byte)255); binaryWriter.Write((byte)0); // application extension binaryWriter.Write((byte)0x21); binaryWriter.Write((byte)0xff); binaryWriter.Write((byte)0x0b); binaryWriter.Write('N'); binaryWriter.Write('E'); binaryWriter.Write('T'); binaryWriter.Write('S'); binaryWriter.Write('C'); binaryWriter.Write('A'); binaryWriter.Write('P'); binaryWriter.Write('E'); binaryWriter.Write('2'); binaryWriter.Write('.'); binaryWriter.Write('0'); binaryWriter.Write((byte)3); binaryWriter.Write((byte)1); binaryWriter.Write((byte)0xff); binaryWriter.Write((byte)0xff); binaryWriter.Write((byte)0); // graphic control extension binaryWriter.Write((byte)0x21); binaryWriter.Write((byte)0xf9); binaryWriter.Write((byte)4); binaryWriter.Write((byte)4); binaryWriter.Write((ushort)100); binaryWriter.Write((byte)0); binaryWriter.Write((byte)0); // image descriptor binaryWriter.Write((byte)0x2c); binaryWriter.Write((ushort)0); binaryWriter.Write((ushort)0); binaryWriter.Write((ushort)2); binaryWriter.Write((ushort)2); binaryWriter.Write((byte)0); // image data binaryWriter.Write((byte)2); binaryWriter.Write((byte)3); binaryWriter.Write((byte)0xc4); binaryWriter.Write((byte)0x14); binaryWriter.Write((byte)5); binaryWriter.Write((byte)0); // terminating character binaryWriter.Write((byte)0x3b); } } } }
We build our Christmas tree
Obviously, the code we've written is hardcoded, and serves us no purpose unless we want to create same file over and over again. We could make our code much more useful if we abstracted this code to take in different .GIF widths, heights, and different pixel values.
Fortunately for you, I've written an application that does just that (and is very-well documented). I explain in more detail what each byte is used for, as well as have comments for the LZW compression of the image data. Now, let's build a Christmas tree (source code is here).
Fortunately for you, I've written an application that does just that (and is very-well documented). I explain in more detail what each byte is used for, as well as have comments for the LZW compression of the image data. Now, let's build a Christmas tree (source code is here).
i am ERIC BRUNT by name. Greetings to every one that is reading this testimony. I have been rejected by my wife after three(3) years of marriage just because another Man had a spell on her and she left me and the kid to suffer. one day when i was reading through the web, i saw a post on how this spell caster on this address AKHERETEMPLE@gmail.com have help a woman to get back her husband and i gave him a reply to his address and he told me that a man had a spell on my wife and he told me that he will help me and after 3 days that i will have my wife back. i believed him and today i am glad to let you all know that this spell caster have the power to bring lovers back. because i am now happy with my wife. Thanks for helping me Dr Akhere contact him on email: AKHERETEMPLE@gmail.com
ReplyDeleteor
call/whatsapp:+2349057261346
i am ERIC BRUNT by name. Greetings to every one that is reading this testimony. I have been rejected by my wife after three(3) years of marriage just because another Man had a spell on her and she left me and the kid to suffer. one day when i was reading through the web, i saw a post on how this spell caster on this address AKHERETEMPLE@gmail.com have help a woman to get back her husband and i gave him a reply to his address and he told me that a man had a spell on my wife and he told me that he will help me and after 3 days that i will have my wife back. i believed him and today i am glad to let you all know that this spell caster have the power to bring lovers back. because i am now happy with my wife. Thanks for helping me Dr Akhere contact him on email: AKHERETEMPLE@gmail.com
or
call/whatsapp:+2349057261346
ReplyDeletei couldn't believe that i would ever be re-unite with my ex-lover, i was so traumatize staying all alone with no body to stay by me and to be with me, but i was so lucky one certain day to meet this powerful spell caster Dr Akhere,after telling him about my situation he did everything humanly possible to see that my lover come back to me,indeed after casting the spell my ex-lover came back to me less than 48 hours,my ex-lover came back begging me that he will never leave me again,3 months later we got engaged and married,if you are having this same situation just contact Dr Akhere on his email: AKHERETEMPLE@gmail.com thanks very much sir for restoring my ex-lover back to me,his email: AKHERETEMPLE@gmail.com or call/whatsapp:+2349057261346
hindi ako makapaniwala na kailanman ay muling makiisa ako sa aking kasintahan, labis akong na-trauma sa pananatiling nag-iisa na walang katawan na manatili sa akin at makakasama ko, ngunit napakasuwerte ako sa isang tiyak na araw upang matugunan ito malakas na spell caster na si Dr Akhere, matapos sabihin sa kanya ang tungkol sa aking sitwasyon ginawa niya ang lahat ng makataong posible upang makita na ang aking kasintahan ay bumalik sa akin, sa katunayan matapos na ihagis ang spell ang aking dating kasintahan ay bumalik sa akin ng mas mababa sa 48 oras, dumating ang dating kasintahan ko. bumalik sa pagmamakaawa sa akin na hindi na niya ako pababayaan, 3 buwan mamaya kami ay nakipag-ugnay at nag-asawa, kung nagkakaroon ka ng parehong sitwasyong ito makipag-ugnay lamang kay Dr Akhere sa kanyang email: AKHERETEMPLE@gmail.com maraming salamat sa sir sa pagpapanumbalik ng aking dating kasintahan bumalik sa akin, ang kanyang email: AKHERETEMPLE@gmail.com o tumawag / whatsapp: +2349057261346
To where to find wps pin on hp printer between HP printer and PC, utilizing a WPS Pin is fundamental. Become acquainted with how to discover it and make it being used.
ReplyDeleteI was diagnosed as HEPATITIS B carrier in 2013 with fibrosis of the
ReplyDeleteliver already present. I started on antiviral medications which
reduced the viral load initially. After a couple of years the virus
became resistant. I started on HEPATITIS B Herbal treatment from
ULTIMATE LIFE CLINIC (www.ultimatelifeclinic.com) in March, 2020. Their
treatment totally reversed the virus. I did another blood test after
the 6 months long treatment and tested negative to the virus. Amazing
treatment! This treatment is a breakthrough for all HBV carriers.
hii
ReplyDeleteThis comment has been removed by the author.
ReplyDelete