As a developer advocate, part of my job as a developer advocate is to inspire others to work with .NET and JetBrains ReSharper and JetBrains Rider. Thus, to inspire others, I need to be inspired. So this search for creative ideas sends me down some strange and wonderful rabbit holes, and I’m glad I get to share them with you.

In today’s post, we’ll see how to use popular OSS projects ImageSharp and Spectre.Console to render a real-time animated GIF to a terminal’s output. The post will be short, but You’ll get to see two crucial code snippets.

The General Idea

We’re going to combine two OSS projects features to produce our animated console graphics. To do that, we’ll be using ImageSharp’s Image class along with Spectre.Console’s CanvasImage. In our first step, let’s extract each frame from a GIF as an image.

Turning GIF Frames Into Single Images

Let’s start by choosing a GIF. But, first, you can head over to Giphy and pick an animation you’d like to render to your terminal. Terminals are typically limited in their color output and image fidelity, so be sure to choose something simple. For example, I’ve chosen this image of aliens dancing.

Alien Dance Party

Now, let’s create a new .NET console application and add the three necessary dependencies.

<ItemGroup>
  <PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" />
  <PackageReference Include="Spectre.Console" Version="0.41.0" />
  <PackageReference Include="Spectre.Console.ImageSharp" Version="0.41.0" />
</ItemGroup>

Next, be sure to add your gif to the project and be sure it is copied to the output directory so our loading process can find the image. You may also hardcode the complete path to the image.

Let’s look at the code required to load and get each frame from a GIF.

using var gif = await Image.LoadAsync("aliens.gif", new GifDecoder());
var metadata = gif.Frames.RootFrame.Metadata.GetGifMetadata();

foreach (var frame in gif.Frames.Cast<ImageFrame<Rgba32>>())
{
    var bytes = await GetBytesFromFrameAsync(frame, cts);
}

async Task<byte[]> GetBytesFromFrameAsync(ImageFrame<Rgba32> imageFrame, CancellationTokenSource cancellationTokenSource)
{
    using var image = new Image<Rgba32>(imageFrame.Width, imageFrame.Height);
    for (var y = 0; y < image.Height; y++)
    {
        for (var x = 0; x < image.Width; x++)
        {
            image[x, y] = imageFrame[x, y];
        }
    }

    await using var memoryStream = new MemoryStream();
    await image.SaveAsBmpAsync(memoryStream, cancellationTokenSource.Token);
    return memoryStream.ToArray();
}

The method GetBytesFromFrameAsync will take an existing GIF frame and map it to a new in-memory image. Once all pixels have been mapped, we can save the output to any file format. In this example, I chose to commit the bytes to a Bitmap in memory. Still, you can choose to save frames to any ImageSharp supported format (JPG, PNG, etc.), along with a more permanent storage mechanism like disk or remote storage.

Great, so how do we render each frame to the console output?

Rendering Images To Console Output Using Spectre.Console

Spectre.Console has live display capabilities, meaning it can overwrite existing output. We’ll take advantage of this by rendering a CanvasImage at the interval defined by a GIFs metadata.

using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.PixelFormats;
using Spectre.Console;

var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, _) => cts.Cancel();

await AnsiConsole.Live(Text.Empty)
    .StartAsync(async ctx =>
    {
        using var gif = await Image.LoadAsync("aliens.gif", new GifDecoder());
        var metadata = gif.Frames.RootFrame.Metadata.GetGifMetadata();

        while (!cts.IsCancellationRequested)
        {
            foreach (var frame in gif.Frames.Cast<ImageFrame<Rgba32>>())
            {
                var bytes = await GetBytesFromFrameAsync(frame, cts);
                var canvasImage = new CanvasImage(bytes).MaxWidth(50);
                ctx.UpdateTarget(canvasImage);

                // feels like anything less than 100ms is slow
                var delay = TimeSpan.FromMilliseconds(Math.Max(100, metadata.FrameDelay));
                await Task.Delay(delay, cts.Token);
            }
        }
    });


async Task<byte[]> GetBytesFromFrameAsync(ImageFrame<Rgba32> imageFrame,
    CancellationTokenSource cancellationTokenSource)
{
    using var image = new Image<Rgba32>(imageFrame.Width, imageFrame.Height);
    for (var y = 0; y < image.Height; y++)
    {
        for (var x = 0; x < image.Width; x++)
        {
            image[x, y] = imageFrame[x, y];
        }
    }

    await using var memoryStream = new MemoryStream();
    await image.SaveAsBmpAsync(memoryStream, cancellationTokenSource.Token);
    return memoryStream.ToArray();
}

It’s that easy! Now you can render GIFs to the console with a few lines of C# code.

Conclusion

While this is a fun demo, I could see folks using it to add splash screens to console applications, build console games with cut scenes, and so much more. One downside to this approach is its dependant on the resolution of the terminal window. A console with small dimensions means you lose the visual fidelity of the original GIF. Another drawback is terminals have a limited color palette, so overly complex GIFs may look incomprehensible.

Well, I hope you give this demo a try, and if you’d like to download the complete sample, head on over to GitHub, where I’ve made the source code available. I’ve also included several other GIFs for you to try out.