Writing My Own Pokémon Spritesheet Generator in C#
For my Living Dex project I wanted to control how the Pokémon spritesheets work. There are existing spritesheet generators, but I wanted to make my own in .NET for fun and learning.

Why?
Early 2022 I had two problems:
- I was originally using the output from pokedextracker/pokesprite (see my post about installing it) via GitHub for the spritesheets used in Making a Living Dex: Appendix A - The Whole Living Dex Roster however I didn't like the direction they took with the Pokémon Legends Arceus icons
- I really wanted a practical excuse to learn some of the newer .NET features
So tada! 🎉
Overview
The work is heavily based on pokedextracker/pokesprite (which in turn is based on pokesprite-gen) and written in C# I've attempted to work in a few of the newer features to give myself a practical example and play pit. The output is two files:
pokesprite.css
pokesprite.png

How it works
At the time of writing this, it isn't finished or cleaned up, but it works™ - and how it works is:
- Get the latest sprites and metadata from the Pokesprite NPM feed
- Scale the sprite if required
- Trim the whitespace, so there's no padding around the sprite
- Generate the spritesheet
- Generate the
.css
file - Copy to output
1. Get sprites
A fun step because I was able to use the new .NET 7 System.Formats.Tar namespace. In fact, I wrote about it here:

We grab the .tgz
file from the pokesprite-images NPM feed, then decompress and load the files into memory. Then those files are filtered and we only grab:
- The
pokemon.json
file, which contains metadata about the Pokémon - The latest Pokémon sprite images, including forms
- All the ball sprites
All these get fed into the next step. We'll be using the Squirtle sprite to showcase from here onwards.

2. Scale
I'm going to be honest here, at the time I was getting a grasp on what the pokedextracker repo was doing and simply one for one threw this code into my project. In the end, none of the sprites that I pick up use this feature - however in the future I might use other sprites that do need to be scaled.
Due to the above, most sprites are just hot-potatoed to the next step without any processing.
No change in the sprite.
3. Trim
Because we want to control the padding ourselves with CSS, we want to remove all transparent tiles all around the sprite.


4. Generate spritesheet
At this point, we have all the sprites we want. We then look for the tallest and widest sprite and use these max tall and max wide values to create the spacing each sprite will have from each other on the spritesheet.
Then each sprite is placed in number order from left to right, top to bottom in 32 columns.

5. Generate CSS
Since we know where each Pokémon is and the trimmed size of the sprite, we can generate CSS:
- To point to the top left of the rectangle the sprite occupies as
background-position
- Have a size the same as the trimmed sprite and normal
width
andheight
Done!
6. Output
The code of this is extremely crude, but essentially it spits out the spritesheet and corresponding CSS with a little bonus: an HTML file to smoke test all the sprites.
What I learned
The code complexity itself isn't too bad (the quality on the other hand... 😰), but I used it as an opportunity to arbitrarily cram in bits of learning. If you want to take the work I've done, feel free to revert these to something more... normal.
System.Threading.Channels
The main excuse for this project. I love the idea of channels and they're used in the sprite manipulation as a sort of asynchronous pipeline.
I had a prototype session messing around in another GitHub repo of mine:
I found them easy to use and super clean. I'll try use them more in the future!
System.Formats.Tar
I originally saw this as a tweet and thought "damn that's a neat idea" and read the linked PR for the .NET repo:
Turns out this project is a great fit because NPM uses .tar
files and I grab the sprites via NPM. I found this new class easy to use and even wrote a post about it:

Specific use cases for using declarations
In .NET it often feels like IDisposable
types are heavily paired with using
statements. Then as of C# 8 (.NET Core 3.x+) we got a new feature: using
declarations. With these you don't explicitly set the scope and these IDisposable
objects are disposed when the outer scope is completed.
See:
// using statements
using (var gzip = new GZipStream(tgzStream, CompressionMode.Decompress))
{
using (var unzippedStream = new MemoryStream())
{
await gzip.CopyToAsync(unzippedStream);
unzippedStream.Seek(0, SeekOrigin.Begin);
using (var reader = new TarReader(unzippedStream))
{
}
}
}
vs
// using declarations
using var gzip = new GZipStream(tgzStream, CompressionMode.Decompress);
using var unzippedStream = new MemoryStream();
await gzip.CopyToAsync(unzippedStream);
unzippedStream.Seek(0, SeekOrigin.Begin);
using var reader = new TarReader(unzippedStream);
I found that when I was doing work with the Graphics
object, I needed to explicitly dispose the object for the data to be written to the Bitmap
object - meaning if I use the using
declaration, the object won't be disposed until the end of the function, which is too late because I want the updated Bitmap
before then.
using var imageStream = new MemoryStream(item.Image);
using var pokemonImage = new Bitmap(imageStream);
using (var graphics = Graphics.FromImage(spritesheet))
{
graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;
graphics.DrawImage(pokemonImage, column * maxWidth, row * maxHeight);
}
I now better understand when to use statements vs declarations.
Local functions
Introduced in C# 7 (Framework and Core 2.x+) were local functions. I don't think the project has a good use case for it, but I wedged it into Npm.cs
to get what the most recent version of the NPM package is.
I'm still not sure about them - outside of ragged program.cs files.
See below with the local function GetLatestVersionAsync()
public async ValueTask<MemoryStream> GetTarball(string packageName, string? packageVersion = null)
{
var version = packageVersion ?? await GetLatestVersionAsync();
var packageMetadata = await GetPackageMetadataAsync<NpmPackageModel>(packageName, version);
var httpClient = new HttpClient();
// returns non seekable stream, is this fine for my purposes?
// if not, can just copy to a memorystream
// https://stackoverflow.com/a/3373614
using var stream = await httpClient.GetStreamAsync(packageMetadata.Dist.Tarball);
var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
return memoryStream;
// messing with local functions
async ValueTask<string> GetLatestVersionAsync()
{
var latestVersion = await GetPackageMetadataAsync<NpmPackageQueryModel>(packageName, null);
return latestVersion.DistTags.Latest;
}
}
ValueTask
This one is more for me to remember they exist. I've seen that some .NET experts are using this almost as their default Task
object instead. So I've tried it here and it works well, except I ran into the problem that Task.WhenAll()
requires a Task
and not ValueTask
.
Raw string literals
I got to mess around with the C# 11 preview feature: Raw String Literals. They're strings that let you put all sorts of characters in it like quotes or backslashes without escaping them - because you cannot escape anything inside a raw string literal.
private string RootClass = """
.pkicon {
@include crisp-rendering();
display: inline-block;
background-image: url("pokesprite.png");
background-repeat: no-repeat;
}
""";
I then used the interpolated form to create the CSS classes.
var cssClass = $$""".pkicon.pkicon-{{item.Number}}{{FormData(item.Form)}} { width: {{item.TrimmedWidth}}px; height: {{item.TrimmedHeight}}px; background-position: -{{column * maxWidth}}px -{{row * maxHeight}}px }""";
Future plans 🤔
Technically I could finish up the code by:
- Cleaning it up
- Use
System.Commandline
- Make it extensible to manipulate all sorts of sprites, rather than just Pokémon and balls
But we'll see, it works as it is right now. If you want to take it and make it great, go ahead!
To Conclude
Thank you for joining me on this little tour through sprite making. I hope you enjoyed it and found something useful along the way.