The Secrets of High Quality GIFs on Discord
Dec 24, 2021
By GLQv192
Background

A few years ago, about a month after RWBY V6 started airing, I found that having short clips of a scene that I could share made it much easier to discuss that scene with others on Discord. I didn't have to struggle to find the timestamp, get a link, or try to explain it with words, I could just drag a file into Discord and the thing being discussed was visible right there for everyone.

However, GIFs are special, and not in a good way. They are pretty convenient, but overall, they just suck. GIFs I saw posted in chat by others often had weird glitches, super low framerate, bad colors, extreme color banding, and I could go on. I wanted my GIFs to not be like that, which meant that I had to figure out how to prevent those things from happening.

At the time, I had no idea how to get access to the actual videos of the episodes, so I would use OBS to record my screen and then just play parts of the episode I wanted to make clips from. I also had no idea what the best way to make GIFs was, so I just used ezgif. The result wasn't bad, but it was far from good. Still, it was way better than most of those other GIFs, so at the time I figured it was good enough.

I then started making more and more GIFs as V6 aired. By the end, I would make GIFs of almost every scene in the episode. The tedium of starting recording, playing the clip, stopping recording, uploading to ezgif, trimming the video, and converting to a GIF over and over really made me want a better and faster way of doing things. I first looked into getting the original video from YT or RT, and found that both ffmpeg and youtube-dl could help with that (use yt-dlp now, it fixes some issues with youtube-dl). I had used ffmpeg a tiny bit before for re-encoding videos, but this made me look into what else it could do. That's when I found out it could also convert videos to GIFs. It solved all of my current problems.

I had a lot to learn to reach an acceptable level of quality for the GIFs. If you simply ffmpeg -i input.mp4 output.gif, the resulting quality is just awful in almost every possible way:

You need filters, you need to trim the video, you need to resize it, you need to generate and apply a color palette, ffmpeg also has lots of weird options that won't make any sense unless you're an expert on various file formats. It took me a while, but I did eventually get at least decent at it, and while learning how to do all of that, I discovered ways of improving the quality of the GIFs beyond what my old method could achieve.

That's what made me think, how much quality could I squeeze out of this awful format if I really tried? Well, after having made thousands of GIFs and my own set of tools to make them since then, here's what I've found out.

Good & Bad

There are far too many things that can go wrong with a GIF to list absolutely everything, so for this post, I'll simplify it to just a scale from "bad" to "good". The "better" a GIF is, the closer it is to the full quality source video. A "perfect" GIF would look exactly like the source video, you wouldn't be able to tell them apart if the video was muted. The limitations of GIFs makes a "perfect" GIF impossible, we can only do "good", but that is at least the goal, and we want to get as close to that goal as possible.

The bad GIF here was made intentionally by me to mimic bad GIFs I've seen posted in Discord. I used my own tools with all of the wrong options I could think of. The good GIF was made the same way, but with the default settings. I think there is one very small improvement that can be made to the good GIF, and that is a slightly better color palette. The better palette wouldn't really change much, I doubt anyone would even notice without looking very closely at it. Ignoring that one thing, I don't think it's possible to create a GIF that is closer to the original video for Discord. Discord adds some extra limitations to GIFs that I'll go into more later.

I have no idea where the bad one here came from, I didn't make it, it's just one of the bad GIFs I've seen posted in Discord. I also have no idea what has happened to it. My best guess is that it has been converted from a video to a GIF and back multiple times. The good one was made using my tools with the default settings again.

Since this one's more authentic, let me go through the biggest problems it has:

The Enemy

The GIF format has a lot of limitations, mostly due to the fact that it wasn't initally meant to do what we use it for today. It was made almost 40 years ago as a compact color image format. Back then, downloading an image that was several megabytes could take a while, so most of the GIFs made then were very simple pixel art sprites, rarely bigger than 128x128, and often without any animation, and the format works pretty well for that. Today, people use it to create short looping clips from videos. Videos of pretty much anything you can think of, not just simple pixel art. The videos might have millions of colors, ultra-high resolution, and 60 frames per second. If you packed all of that into a GIF, it could end up being several gigabytes.

The format is just outdated. Today we have much better ways to compress image data, and some of the design choices are a bit weird and could be improved. The way the framerate is stored is one of those weird things.

Technically, the highest framerate allowed by the GIF format is 100, but as far as I know, there aren't any apps that will accept 100 FPS GIFs. Most will just play them at a much slower rate. The second highest framerate, and the highest accepted by most apps, is 50. The third highest is 33.333... There is nothing in between. This is because GIFs store the framerate as a delay until the next frame, either globally or per frame, as a 16-bit unsigned integer counting hundredths of a second. So, a delay of 1 would be 100 FPS, 2 would be 100 / 2 = 50, 3 would be 100 / 3 = 33.333... (A delay of 0 wouldn't really make any sense, even though it is technically possible to set those two bytes to 0). This causes an issue when converting videos to GIFs, because common framerates for videos are 24, 29.97, 30, 48, 59.98, and 60. None of these can be reached by dividing 100 by an integer. There are some close calls, 24 is almost 25 (100 / 4), 30 is almost 33.33... (100 / 3), and 48 is almost 50 (100 / 2), but none of them are exact. This means that GIFs converted from videos will usually either play at a different rate or they will stutter ever so slightly. The stuttering comes from setting the delay per frame so that the average matches the original video, this usually means making some frames display for 0.01 seconds longer or shorter than the rest.

The number of colors is also limited in GIFs. GIFs store each pixel as a byte, meaning each pixel can have a value between 0 and 255. Those values represent an index in the GIF's color palette, which can hold 256 different RGB colors (one can be fully transparent). That means you have up to 256 colors to work with, which is... slightly less than the millions of colors most videos have. There are ways to get more colors in a GIF, however. Each frame is allowed to have its own color palette. This is great if the colors change a lot between each frame, but it's still only 256 colors per frame. If you're familiar with GIFs and other tools that make GIFs, you're probably screaming "GIFSKI!" right now, but I have some bad news for you. gifski is a tool that uses a less known (or at least less used...) feature of GIFs, which is that frames can be divided into smaller blocks, and each block can have its own color palette. gifski uses this to create true color GIFs. These GIFs lose no color information at all, meaning you can keep the millions of colors from the original video if you want. Sounds good? Yeah, a bit too good. There are two massive issues with this, one is purely for Discord, and the other is a bit more general:

  1. File size. GIFs already get big very fast, and adding all of these extra colors results in some massive GIFs. This is not only bad for Discord, where there is an 8 MB limit for non-Nitro users on non-boosted servers, it's also just bad in general. It means more storage is used per GIF (unfortunate when you have thousands of them), using these GIFs on a website will result in slower loading times for visitors, and uploading them anywhere takes longer.
  2. Discord will not display true color GIFs, just a reduced version of them with fewer colors and a lot of color banding. That extra file size is all for nothing if you want to use the GIF on Discord.

This leads nicely into Discord's problems with GIFs. When you upload a GIF to Discord, you'll see a small message saying that it is being processed before it actually shows up in the chat. During this time, the GIF is handed to Discord's Media Proxy, which uses their own image resizer, Lilliput. Any GIF will go through Lilliput, even if it doesn't need to be resized, because it doesn't just resize them, it also tries to compress them a bit. The compression is actually a side effect of their resizing algorithm. What happens is that the color values are rounded to the nearest value that has a remainder of 4 when divided by 8. Think of it like dividing the numberline into blocks of 8 numbers, and then get rid of 7 of those numbers in each block, only keeping the 4th one, and that 4th number then represents that whole block. Lilliput's code refers to these rounded values as being "crushed", so I'll do the same from now on. For the 8-bit color values, that means going from 256 different values to just 32. Lilliput uses this reduction to speed up the resizing of the GIF, there are just a lot fewer numbers to keep track of. As a sidenote, this actually means that GIFs in Discord never show true black (0, 0, 0) or white (255, 255, 255), the closest they ever get is 4, 4, 4 and 252, 252, 252.

The colors are not the only thing getting messed up by Lilliput, however. Since the workaround is stupidly simple and should honestly be done anyway, I haven't looked into exactly what Lilliput does when scaling down GIFs, but whatever it is, it's pretty bad:

As you can probably see, it doesn't always work that well. The original GIF that it resized here looks alright, but when posted in Discord and processed by Lilliput, it turns into the mess above. This happens for almost all GIFs that use ordered dithering and gets scaled down by Lilliput. There are other weird things that can happen to GIFs scaled down with Lilliput, but this is by far the most common and biggest issue.

Lilliput also puts restrictions on the duration and frame count of the GIFs. These values can change, but as of writing this, the maximum number of frames a GIF can have on Discord is 1,000. I haven't tested the maximum duration, but I know that it is at least more than 60 seconds.

Recently, an issue caused by Lilliput was reported to me that I've never seen before by someone using my tools. Because of how rare this is, and how little time I've had to look into it, there is currently no known way of fixing this (by me, at least), but I thought it was worth mentioning. It's hard to explain what happens with just words, so here's a GIF affected by this thing:

As you can see, some pixels seem to stick around when they shouldn't. Similar issues often happen with GIFs that have transparency, and that is caused by using the wrong disposal method, but this GIF doesn't have transparency, and the disposal method is correct. I have no idea what's happening. I did find some kind of workaround, but it's not perfect and it has its own issues, which is to generate a palette per frame. With GIFs that don't change much with each frame, that can cause flickering.

The Solution

Alright, so GIFs are bad, Discord makes them worse, and there's seemingly no end to the problems. How can any of this be fixed? Well, it turns out some of the problems have an easy workaround, some are not as bad as they sound in most cases, and some can be overcome by fighting fire with fire.

Let's get some of the easy ones out of the way first. The framerate is not a big deal. Getting the average framerate to match the original video works great, it's usually very hard to notice the tiny stutters caused by it. There's not much we can do about higher framerate videos. I usually just skip every other frame in 60 FPS videos and make the GIF play at 30 FPS.

Everything caused by Lilliput actually resizing the GIF can be ignored completely if we just resize the GIFs ourselves. They must fit into a 400x300 box to not be resized, so for a standard 16:9 GIF, the max size would be 400x225. A square GIF would be 300x300.

There's nothing we can do about the duration and frame count limits, but they rarely matter anyway. A GIF with more than 1,000 frames would most likely be too long for anyone to care, and the file size could be massive. I usually try to keep my GIFs under 240 frames (10 seconds of 24 FPS video) anyway.

And now, onto the slightly more complicated problems. Lilliput crushing the color values is a big problem. It means that even the normal 256-color GIFs will often get reduced to even fewer colors. Having only 256 different colors to work with is bad enough, so what can we do to stop this? The solution is actually quite simple, but to understand why it works, we need to look at how color palettes are generated and applied to images.

A regular image might have millions of colors and we need to reduce that to just 256. This process is called color quantization. There are several ways to do this. You could gather them all and just take 256 random colors from the pile, for example. That probably wouldn't work that well in most cases, though. You could have an image with a lot of different dark colors and some bright lights here and there, and if you randomly picked just a few colors from that, you would most likely just end up with only dark colors, so you wouldn't be able to draw those lights at all. Since I'm using ffmpeg to do all of this processing, I'll mention that it uses the median cut algorithm. What this process outputs, the reduced list of colors, is what's important to solving the issue with Lilliput.

When you're applying a reduced color palette to an image, you could just take each pixel and replace the color with the closest one from the palette, but that will not look very good. It produces a lot of color banding, and the wikipedia article about it has an excellent illustration showing the problem and the solution to it: dithering. There are many ways to dither, but what I usually use is ordered dithering. If you're familiar with the paletteuse ffmpeg filter, I use the bayer algorithm with a scale of 3. This deals with the color banding pretty well while making the pattern not too obvious. The reason I use this is that it generally makes the GIFs' file size much smaller, and it avoids issues like flickering/wandering pixels that comes from error diffusion dithering.

But I said the solution was quite simple, didn't I? Why do you have to know all this stuff? Well, it's because it needs to happen in the middle of it all. The solution is to fight fire with fire. We do exactly what Lilliput does, but to the palette instead of the GIF. The color quantization process can be modified in a way that it outputs 256 colors that are already crushed, and if that is then applied to a GIF Lilliput will try to crush those values again, but they won't change, because they're already crushed. It's the same as trying to round 1 to the nearest whole number, it will still be 1. But does that change anything? If we're doing the exact thing we're trying to avoid, isn't that just as bad? No, the order here is very important. Here's a GIF crushed by Lilliput, and a GIF crushed by Lilliput, but with a palette that's already crushed:

If you don't see it, look at the background. I should mention that I'm using error diffusion dithering here to make the effect more obvious. It's not as severe when using ordered dithering. The reason crushing the palette works is that ffmpeg will dither nicely with whatever colors you give it when applying a palette. Here I've limited the maximum number of colors to just 16 instead of 256, and the background still looks smoother than the crushed GIF:

For reference, the original downscaled frames had 167,365 colors. This shows that what palette you apply to the image doesn't matter that much for quality before it's put through Lilliput, but with the right color values Lilliput won't be able to do anything to mess it up. This is also why gifski GIFs look so much worse in Discord, by the way. gifski does not dither because it doesn't need to.

I talked briefly about ordered dithering and error diffusion, but I just want to add an example showing what I don't like about error diffusion dithering for animated GIFs specifically. It's fine, maybe even better, for still images, but when things move around, error diffusion does too:

The file size is almost three times that of the GIF above it with ordered dithering, and you can see random pixels dancing around everywhere. There is still the problem I mentioned earlier about Lilliput destroying GIFs with ordered dithering, but if we don't let it resize the GIF, it works perfectly fine, so it's not really an issue we need to worry about.

The result of all of these changes is that when you post the GIF in Discord, Lilliput won't change anything about how it looks. It will look the exact same in Discord as the original GIF you made.

And finally, limiting the number of frames, making sure the size is small enough to avoid Lilliput's resizing, crushing the palette, and using ordered dithering all help a lot with making sure the GIF stays small, so file size is usually not a big problem. There are some cases where you'd want to clip a long shot with lots of moving parts where the size can be an issue still, so here are some things to keep in mind when working to reduce the size even more:

Post Processing

Good GIFs are not invulnerable. Discord's Lilliput might not damage them, but they can still be utterly destroyed by other things, so you need to watch out for that. Tenor is all about quantity over quality. You won't find a single high quality GIF on Tenor, and there's a reason for that. When a GIF is uploaded to Tenor, it goes through some processing, just like what happens with Discord and Lilliput, except Tenor's processing is much worse for the quality of the GIF. The process is very lossy, no matter what you do to the GIF, it will lose quality. This edit I made a while back was stolen by someone and uploaded to Tenor, and here's what happened to it:

It's blurry, lots of small details are just lost, the colors are a bit off in some places, and it also somehow has the wrong aspect ratio.

Tenor is not the only thing that can destroy a good GIF, however. Imgur does not destroy GIFs, but if you post the link to a GIF on Imgur, the embed will most likely show a video of the GIF instead of the GIF itself, and that video will be lower quality. Gfycat is just as bad as Tenor, if not worse. It absolutely destroys GIFs. Don't use it.

I haven't tested other sites like these, but just remember that most of them will probably ruin your GIFs. Test them first, and pay very close attention to the quality of the output compared to the input before sharing the GIF from there.

Another thing that can damage good GIFs is editing. All editing should be done on the original frames of the video before being turned into a GIF. If you edit a GIF directly, or extract the frames of a GIF and edit those, the output will look much worse, because the GIF already has a good palette, and if you do anything to the image, that palette is no longer the right palette, so a new one should be generated. But generating a palette from a paletted image with some edits done to it will not be good for quality.

Speaking of editing GIFs, this is what Microsoft's Photos app does to perfectly good GIFs if you just use it to draw a smiley face on them:

I should also mention that the size of the original was just 1.39 MB, while this one is 5.05 MB... Avoid this app like the plague.

Another thing to not ever do is converting the GIF back into a video. If you want a good looking video of the same thing, just make it from the original source, not the GIF. Same goes for converting the GIF to any kind of other format. GIF is a bad format with lots of limitations, don't bring those to other formats.

Perfection

If good is not good enough, and you seek perfection, you must drop GIFs entirely. There is simply no way to turn a random video into a GIF that looks just like the video. So, what looks just like a video? Well, a video of course. Unfortunately (or maybe fortunately), Discord will not autoplay videos posted in the chat. This is very good because videos often have sound as well, and I'm sure the millions of Discord users would not want random jumpscares from videos automatically playing contantly. It will instead show a video player interface that plays the video when you click on it, and the video will not loop. However, soundless videos are a thing, and they can be set to loop. Discord even uses these soundless, looping videos when embedding things like Tenor and Imgur "GIFs". They are actually MP4s or WebMs, not GIFs. So, how can we use that?

I said uploading GIFs to Imgur may not be the best idea, but it also lets you upload MP4 videos. Perfect, so we can just upload the original source video and post it in chat... Well, no, it's never that easy. When Discord plays videos in the chat, it scales them to fit that 400x300 box, similar to what it does with GIFs, except there's no processing going on there. It's playing the video that was linked, but your web browser or Discord client scales it down to fit the box automatically. Some web browsers might do this well, most won't, at least not in certain situations, and the Discord client won't either.

I won't focus more on web browsers, just the Discord client, but it is built on Chromium, so expect the same to be true in browsers like Chrome and Edge. When a video is scaled either through CSS or the HTML video attributes, it won't look the same, because it's a different size of course. But there are better and worse ways of doing this scaling. Normally the video is scaled down by applying an anti-aliasing filter to the image, essentially blurring it slightly, before sampling pixels to reconstruct the image at a smaller scale. That blur is very important, because without it, the downscaled image will have a lot of awful looking aliasing artifacts, also known as jagged edges. However, in some situations, that blur will not be applied. I'm not entirely sure why, it's just something I've noticed with Chromium, it might happen on other browsers too, but I'm not sure. Discord will never display the videos in the right conditions for the blur to be applied, so when a video that can't fit into the 400x300 box is posted, it will get scaled down without the blur and it will have all of these aliasing artifacts.

The solution to this problem is again to do the scaling yourself, except this time there's an extra step required. If you upload a small video that can fit into that 400x300 box to Imgur, the processing done on the video will destroy the quality, so we're back to having a bad looking clip. However, I've found that if you scale the video down to fit the 400x300 box, then scale it back up to twice the size of that with nearest neighbor interpolation, and then upload that to Imgur, the quality difference when viewed in Discord will be close to none. This is because each pixel in the scaled down video will turn into 4 pixels in the final video you upload to Imgur, meaning that any processing done by Imgur has to damage 4 times as much information to really make a big difference, which it probably won't ever do. Another nice thing about this is that MP4 videos are much better at compression, so the output file is smaller than the GIF version. So we did it, we reached perfection for autoplaying, soundless clips in Discord. The only problem is having to manage all of the links to your clips instead of just having a folder of GIFs to drag into Discord. I still prefer the GIFs because of this.

You may notice they play at slightly different rates. This is because the video is playing at exactly 24 FPS, like the original, while the GIF does the averaging trick to get as close to that as possible.

In a Better World

I mentioned earlier that we have better ways to compress image data today, and that the GIF format has a lot of issues, so I wanted to talk about the alternatives to GIFs and why we can't use them.

I'm sure you've heard of animated PNGs, or APNGs. These things are pretty cool, and they're supported by most web browsers and the Discord client. They have a better way of storing the framerate than GIFs, meaning you can actually have an APNG playing at exactly the same rate as the original video. They are just like normal PNGs, but animated. They can have semi-transparent pixels. They can have up to 16 bits per channel, that's 64 bits per pixel, meaning 18.4 quintillion different colors if you include the alpha channel, just a tad more than 256... Also most likely more than your monitor can display. However, APNGs do also have downsides. The compression could still be better. Adding all of these extra colors doesn't help with the file size, either. If you make a good GIF from a video and then try to make a good APNG from the same video, the APNG will almost certainly be way bigger than the GIF because of the extra colors. Also, even though Discord can and does support APNGs, they're even used for stickers, it will only ever show the first frame of APNGs attached to messages. This is most likely due to Discord wanting people to pay for Nitro. People actually got around buying Nitro to get animated profile pictures before they changed APNGs to only show the first frame, and now stickers are also a thing that you need Nitro to have access to, so don't expect APNGs to ever become useful in the same way as GIFs on Discord.

WebP is also a very good format that Discord and most web browsers support already. It compresses better than PNGs, while keeping all of those nice features, except the 16-bit color depth, it only supports 8-bit, but that's just as much if not more than most videos you'll find. It also supports lossy compression that looks alright, though it's still lossy. However, Discord will display still WebPs just fine, but if you try to post an animated WebP, it will just show the loading spinner animation and never anything from the WebP. This is probably in a similar boat as APNGs.

WebP 2, AVIF, JPEG XL (aka JXL), and probably more formats are being worked on that further improves the compression while keeping things like animation support and non-paletted colors, but support for these are not great at the moment. Chrome and Firefox has support for AVIF images, but Firefox doesn't support animations yet. Chrome, Edge, Firefox, and Opera let you enable support for JXL in the settings, but it is not enabled by default. WebP 2 just started being developed half a year ago, so nothing has support for it yet. Don't expect Discord to support any of these in the near future, or at all.

JXL has to be what I look forward to the most out of these currently. Don't let the JPEG name scare you away, it supports lossless compression. It has potential to be the next PNG, JPEG, and GIF all in one format with better compression and more features. In a better world, any time we see .png, .jpg, or .gif at the end of a file name, we'd see .jxl, and the file would be smaller and, in a lot of cases, better looking.

Here are a few images with some of these formats so you can see what your browser supports. I wouldn't pay too much attention to the file size of these (which you can see if you hover over them, same goes for any GIF on the page), I used ImageMagick to convert the PNG to the other formats, and as far as I know, it doesn't support everything some of these formats can do, but they're still all smaller than the PNG. Also, none of these are animated because of the same reason.

Your browser does not support PNG, I feel bad for you
Your browser does not support WebP
Your browser does not support AVIF
Your browser does not support JPEG XL
Conclusion

GIFs suck. Discord makes them suck more. But right now, it's the best we have access to unless you like collecting links to videos on Imgur. Hopefully GIFs will eventually stop existing and something better will take its place, but it hasn't happened yet and I don't see it happening anytime soon.

Anyway, I hope you learnt something from this, even if you're not making GIFs. I always like hearing about weird technical details like some of these things, and I hope you got something out of it all. This post ended up being a lot longer than I thought it would, but there are just so many things to talk about in this topic, and I wanted to go through most of it. I didn't get into transparent GIFs and their issues much, but that would probably be its own section because of how much can go wrong with those and how tricky fixing them can be.

I'll also say that you should always keep all of the frames in a GIF. For 60 FPS videos, sure drop it to 30 FPS so the GIF actually works, but for videos with 50 or less FPS, keep the frames. I see so many GIFs everywhere that are missing most of the frames of the original video, and I think that ruins the GIF more than most other things.

Hopefully that new Lilliput issue either just goes away or I find a fix for it soon, but that's really all I have to say about this for now.