Skip to main content

Creating .GIFs in C#

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!

Christmas tree .GIF
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.

.GIF file format diagram (credits to http://www.matthewflickinger.com)
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.

Portal 2 multiplayer screenshot
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.

A 32 bit integer in C#
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.

A sample .GIF file
Our sample .GIF we are making

Header

The first set of bytes are the header. The header is 6 bytes long whose values are:
47 49 46 38 39 61
When 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 (credits to http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp)
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 00
How 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.
ff ff ff 00 00 00 ff 00 00 00 ff 00
White 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 00
100 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 00
The 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
Lastly, the image data in code.
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.
Our .GIF file in hexadecimal
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)

Comments

  1. 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










    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










    ReplyDelete








  2. i 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

    ReplyDelete
  3. 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.

    ReplyDelete
  4. I was diagnosed as HEPATITIS B carrier in 2013 with fibrosis of the
    liver 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.

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete

Post a Comment

Popular posts from this blog

UI redesigns are mostly a waste of time

To preface the article, I primarily work on, and prefer, back-end code. I've been involved in both web and software development for over 4 years now and worked with many front-end and back-end frameworks. New Twitter UI Before all of the UI designers that read this go out and riot and champion against me for saying UI redesigns are a waste of time, let me say that I do value design . I think at the bare minimum, a product or website needs to be usable , and if you possess a good eye and steady hand , you should feel compelled to create something that looks pleasing. David Just stop redesigning the UI all the time . UI redesigns, in my opinion, are a waste of time 95% of the time. Let me explain further. No one cares Come see our fresh new look ! What about our new  material design , come see! I'm sorry, but besides fixing the UI where it impacts the usability of your application, no one is raving about how a redesign makes the application any better. ...

[Fix] - ASUS PCE-AC68 adapter (no internet)

There seem to be a lot of problems with this adapter, even with such strong performance . Why so many issues? I'm not quite sure, but I needed to find a fix because I kept on losing wifi. The ASUS PCE-AC68 The fix Keeping it short - this is how I fixed the issue: Downloaded the driver for my OS from ASUS's support page -  https://www.asus.com/us/Networking/PCEAC68/HelpDesk_Download/ (in my case it was Windows 10 64-bit). Open Device Manager by holding the Windows key and pressing R, then typing "devmgmt.msc" and hitting Enter. (Don't worry, this isn't a scam . We are simply opening Window's Device Manager through the Microsoft Management Console snap-in .) Navigate to the yellow warning sign sitting under Network adapters and right click it. Select Update driver . Select Browse my computer for driver software  and choose the following path of the OS that you have installed on your computer. (The path for the driver on my computer was C...

Logging into a website with Powershell

Powershell is great, and it's lately been my go-to shell while I'm working on Windows. Sorry command prompt I really don't do a lot of work in the shell, but I do like to play with low-level interfaces from time to time. The article is about Linux shells, but goes into good explanation about what a shell is if you don't know. Log into a website Today, I wanted to do something that I have never really tried before and that is logging into a website using Powershell . The concepts behind this are quite simple really, as Powershell has support to send HTTP requests  and that's usually all we need, unless the server has CSRF protections in place (which it should). We are going to attempt  to log in to my favorite website for buying socks, Absolute Socks : Is that a turkey?! In order to do that, we need to have a login. So if you don't already have an account on www.absolutesocks.com , go make one now. Viewing the login request On websites,...