TextFX - The Art and Science of Text Mode Conversion

Note: this is a total rewrite of this page and only refers to TextFX8. The older writeup is available here.




TextFX textmode graphics library, revision 8

Rewrite, simplification, new filters.

A Bit of History

Once upon a time, back in the mid 1990's, I figured, let's make text mode demos. I wasn't the first, or the last, with this idea, but I organized a text mode demo competition to make other folk make these demos too. Most of the text mode demos are based on real-time RGB bitmap to text mode conversion. TextFX is my approach for this.

Back then text modes were full screen, and the most typical text modes were 80x50 or 80x43 (the latter being an EGA mode which sometimes looks better). But let's just say 80x50.

The glyphs in text mode are all fixed-width, and you've probably seen the font somewhere; here's the font, dumped from some VGA BIOS long time ago:

The palette looked like this:

Since then, DOS has died, windows 95 has died, windows 98 has died, windows 2000 has died, windows XP has died.. and over time Microsoft also killed full screen text modes, and as an insult to injury changed the palette to:

The change may be subtle at a quick glance, but it really does make a difference. If you try to look at some old ANSI art with the modern palette, they just look a bit off.

Algorithm Explanation

So the basic idea is to take in a RGB image and try to represent it in text mode by picking the glyph, background and foreground colors for the 80x50 text mode.

Over time I developed a bunch of different filters to convert RGB buffers to text mode. Since so much data is lost, there's no correct way of doing it. This page will go through the filters at an approximate age of their development.

Click on the images to see the full resolution.


The BlockColor filter uses only the 25% and 50% dither blocks. It produces pretty good color, pretty bad resolution and is generally rather blocky. Something like 90% of all textmode demos out there use BlockColor, or some variation thereof.

It was one of the first conversion filters. The input RGB buffer for this mode is 80x50; 160x100 version simply resamples the buffer down to 80x50 before use.

Summary: Lots of color, easy, blocky.


The AsciiArt filter uses 160x100 input buffer. Each glyph uses 4 pixels. The lightness of the four pixels is used to try to guess which ASCII character would fit there the best. When this filter was first used ages ago people didn't believe it was real time. Most recently it was used in "Litterae Finis", where the whole demo used just this filter.

The basic idea here is to cut all glyphs into four pieces and count the lit pixels, and then build a look-up table of the glyphs for each combination of four pixel values.

So for the sake of an example let's consider source pixels that are either on or off. In that case we'd have 2222 or 16 different combinations: 0000, 0001, 0010, 0011 ... 1110, 1111. The AsciiArt filter uses 16 levels of grey per quadrant, so the size of the lookup tables is 16161616 or 64k entries.

One problem with this is that the number of lit pixels per glyph is rather limited, and in some cases the pixels don't even hit all the quads..

The solution is to blur the glyphs:

This both increases the range of values per quadrant as well as makes the pixels that are near the edges of a quadrant affect the other quadrants too. To get this even further it would be nice to know what the glyphs around this one..

Summary: There's no color, the resolution is relatively high, but primarily it's an artistic choice.


The HalfBlockColor filter is based on an idea by Jetro Lauha; simulate 160x100 resolution by using the half-wide or half-tall blocks. It takes in a 160x100 buffer and tries to figure out whether dividing the 2x2 cell horizontally or vertically is better. It was first used in "Turing Machines Didn't Care".

No look-up table is needed. Simply brute-force find the solutions for the horizontal and vertical choices and pick the one with least error.

Summary: There's exactly 16 colors, the resolution is pretty good.


LongRamp is a variation of the BlockColor. Instead of using just the two dither blocks, the LongRamp uses a ramp of various characters. Thus, it has more colors (in theory anyway), but the result is not as smooth.

Summary: Still blocky, more colors. More of an artistic choice again.


When TMDC20 was approaching I considered new filters again, this time taking the approach of stupidly large lookup tables. Hires is a greyscale filter which takes in 320x200 buffer and, using a huge lookup table, tries to fit glyphs. It only does a few shades of gray.

The hires filter chops the glyphs into 4x4 bits, and there's three levels of grey per pixel. That means there's 3333 * 3333 * 3333 * 3333 elements, or 43046721 (or about 43 million) elements in the lookup table. Calculating the lookup table takes quite a while as brute-force, and even compressed the data takes about 19 megabytes. I guess it could be possible to make the calculation faster, but I did not bother.

The lookup table calculation uses the whole glyph set as well as all color combinations. By reducing the glyph set and colors it would be possible to turn this filter into a better AsciiArt filter (and the lookup table would probably compress better, too).

Summary: Arguably highest resolution result, but no colors.


Color2x2 is another stupidly large lookup table filter. Instead of resolution, it goes for color. There's only a few levels of brightness, but it's a true 160x100 filter and it does color. It was used in the "Light" demo.

Using 2 bits of brightness for R, G and B for each of the four quadrants which leads to a fun calculation of 444 * 444 * 444 * 444 or 16777216 elements. Like with hires, the brute-force calculation takes quite a while.

The results were not as nice as I had hoped, so I tried adding a weight for each of the pixels in the glyph quadrants. After a few iterations of these I ended up with the one I ended up using (called "resolation" for no particular reason). All of the weights I used can be found in the tfx_color2x2.cpp, so if you wish to check them out, feel free to waste some CPU cycles =)

Summary: Color, high resolution, but somewhat coarse.


Post-TMDC20 discussion about filters made me realize it would make sense to use separate filters for edges and separate for flat areas. This quickly made filter does just that; it runs the input buffer through both BlockColor and HalfBlockColor, does an edge detect pass on the source buffer and picks between the two filters as needed. The result is surprisingly good.

Summary: High-ish resolution, relatively clean output, colors.

BlockMixer with LongRamp

The BlockMixer could use any two filters, and if TextFX is initialized with LongRamp instead of BlockColor lookup table, this is the result.

Summary: Shrug.

Any comments etc. can be emailed to me.