Burrows Code Blog

May 4, 2010

Display 24-Bit Bitmaps in the Windows Console

Filed under: General Programming — Tags: , , , , , — burrowscode @ 3:01 am

I’ve been working on some code over the past couple hours that will take a bitmap and display it in the Windows console. The code does still have one distinct fault, which I’ll remark on later in the post. My inspiration for this project was primarily Youtube’s April Fool’s Day Joke. While their algorithm is substantially more impressive than what I’ve done (their algorithm works on videos and accounts for color much better than mine), my code is a decent example of how you may go about consuming a file type in an unconventional fashion.

The bitmap file format is in general quite simple. There is a small header that describes details such as image resolution, image dimensions, and the color palette. Our code is designed to work exclusively with 24-Bit Bitmaps, this means that each pixel in the bitmap is represents by 24 bits, which in general is going to be 3 bytes (there are obviously systems that don’t have 8 bit bytes, but we will not be considering these cases). Following this header we get our actual bitmap data. For more information on the bitmap file format you should check out BMP Wiki. Something else you may be interested in is some information on the RGB Color Scheme, so RGB Wiki.

Most of the code is straightforward but I will go through and explain some of it. Initially we parse the bitmap file and setup a descriptor which allows us to more easily access important elements of the bitmap file format. After this we go through and remove the zero padding that the format allows for alignment reasons. Finally we go and actually begin creating the console representation of the bitmap. In order to do this we create a buffer that correlates color to location. Our color converting algorithm is simple, and I didn’t spend much time determining what the optimal DELTA value is (the DELTA value is used to determine if colors have similar intensities) for the application.

Now we take this buffer and pass it to our output function. This is where we have to deal with the fault that I mentioned earlier. Basically it comes out to our images being displayed flipped upside down. This occurs because bitmap data is stored from bottom to top (but still going across rows from left to right) as opposed to top to bottom. All it would really take to fix this is a small adjustment to the output function to print the information in the appropriate order. I however don’t have the time to do this right now, and don’t feel a huge desire to do it either.

The only other known issues are it takes a while to convert big bitmaps and printing colors that are very mixed (in terms of RGB) will result in nonsensical images due to the scaling of pixels to characters.

At any rate here is the code and some example translations (the code has MinGW/GCC and Windows specific segments).

//24 Bit-Bitmap to Console
// Creative Commons - Aaron Burrow

// This software is reliant on both the Win32 and the MinGW Compiler.
// It uses Windows specific functions/structures in order to use colors in the Windows console
// The software also uses the MinGW attribute packed which is only available on gcc like compilers
// On MSVC you would need to use pragma packed
// This code also relies on the the ASCII character set being used
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <math.h>
#include <windows.h>

#define BMP_BITS 24
#define CONSOLE_WIDTH 80
#define CONSOLE_HEIGHT 25
#define NEWLINE 50
#define DELTA 20				// Adjusting this number can have a big effect on how colors are displayed
#define END_DATA 100

// Console Colors
enum Console_Colors {
	BLACK = 0,
	BLUE,
	GREEN,
	AQUA,
	RED,
	PURPLE,
	YELLOW,
	WHITE,
	GRAY,
	L_BLUE,
	L_GREEN,
	L_AQUA,
	L_RED,
	L_PURPLE,
	L_YELLOW,
	B_WHITE
};

// Header definitions taken mostly from Wikipedia, with some small changes
struct bmpfile_header {
	uint16_t magic;
	uint32_t filesz;
	uint16_t creator1;
	uint16_t creator2;
	uint32_t bmp_offset;
} __attribute__ ((packed));

struct bmp_dib_v3_header {
	uint32_t header_sz;
	uint32_t width;
	uint32_t height;
	uint16_t nplanes;
	uint16_t bitspp;
	uint32_t compress_type;
	uint32_t bmp_bytesz;
	uint32_t hres;
	uint32_t vres;
	uint32_t ncolors;
	uint32_t nimpcolors;
} __attribute__ ((packed));

struct bmp_descriptor {
	struct bmpfile_header file_hdr;
	struct bmp_dib_v3_header dib_hdr;
	unsigned char* bmp_data;
};

long read_file(char* path, char** buffer);
long setup_descriptor(char* raw_data, struct bmp_descriptor* desc);
bool create_console_version(struct bmp_descriptor desc, unsigned char* console);
bool validate_bmp(struct bmp_descriptor desc);
int convert_color(unsigned char* bmp);
void print_bmap(unsigned char* buffer);
bool close(int x, int y);
int imax(int x, int y);
int remove_padding(unsigned char* data, int len);

int main(int argc, char** argv)
{
	long file_size;
	char* buffer;
	unsigned char console_buff[25*26 + 1];
	struct bmp_descriptor desc;
	if (argc == 2) {
		if (file_size = read_file(argv[1], &buffer)) {
			if (setup_descriptor(buffer, &desc)) {
				desc.dib_hdr.bmp_bytesz = remove_padding(desc.bmp_data, desc.dib_hdr.bmp_bytesz);
				if (create_console_version(desc, console_buff)) 
					print_bmap(console_buff);
				free (desc.bmp_data);
			}
			free (buffer);
		}
	}
	else 
		printf("Proper usage: %s path.bmp\n", argv[0]);
	return 0;
}

// Using this function requires you to free the file buffer after you are done using it
long read_file(char* path, char** buffer)
{
	FILE * file;
	long size;
	if (file = fopen(path, "rb")) {
		if (!fseek(file, 0, SEEK_END)) {
			if ((size = ftell(file)) != -1L) {
				rewind(file);
				if (*buffer = calloc(size, sizeof(char) + 1)) {
					if (fread(*buffer, sizeof(char), size + 1, file)) {
						fclose(file);
						return size;
					}
					free (*buffer);
				}
			}
		}
		fclose(file);
	}
	return 0;
}

// On a successful use of this function then the caller is responsible for freeing desc->bmp_data
long setup_descriptor(char* raw_data, struct bmp_descriptor* desc)
{
	memcpy(&(desc->file_hdr), raw_data, sizeof(struct bmpfile_header));
	memcpy(&(desc->dib_hdr), (raw_data + sizeof(struct bmpfile_header)), sizeof(struct bmp_dib_v3_header));
	if (desc->bmp_data = malloc(desc->dib_hdr.bmp_bytesz)) {
		memcpy(desc->bmp_data, raw_data + desc->file_hdr.bmp_offset, desc->dib_hdr.bmp_bytesz);
		return desc->dib_hdr.bmp_bytesz;
	}
	return 0;
}

// console must have at least 25*26 writable bytes, and END_DATA needs to be accounted for
// so at least 25*25 + 1 bytes
bool create_console_version(struct bmp_descriptor desc, unsigned char* console)
{
	if (validate_bmp(desc)) {
		int active_width, active_height;
		if (desc.dib_hdr.width > desc.dib_hdr.height) {
			active_width = CONSOLE_HEIGHT;
			active_height = (int)(((float)desc.dib_hdr.height / (float)desc.dib_hdr.width) * CONSOLE_HEIGHT);
		}
		else {
			active_height = CONSOLE_HEIGHT;
			active_width = (int)(((float)desc.dib_hdr.width / (float)desc.dib_hdr.height) * CONSOLE_HEIGHT);
		}
		
		int print_pix = ceilf((float)desc.dib_hdr.width / (float)active_width), main_index = 0;
		for (int i = 0; i < desc.dib_hdr.height; i += print_pix) {
			for (int j = 0; j < desc.dib_hdr.width; j += print_pix) {
				unsigned char* loc = &(desc.bmp_data[i*(desc.dib_hdr.width)*3 + j*3]);
				console[main_index++] = convert_color(loc);
			}
			console[main_index++] = NEWLINE;
		}
		console[main_index] = END_DATA;
		return true;
	}
	return false;
}

bool validate_bmp(struct bmp_descriptor desc)
{
	return desc.dib_hdr.bitspp == BMP_BITS && desc.dib_hdr.compress_type == BI_RGB && desc.dib_hdr.ncolors == 0 && desc.dib_hdr.nimpcolors == 0;
}


// Only the first 24 bits are relevant
// Byte 1 = Blue, Byte 2 = Green, Byte 3 = Red
int convert_color(unsigned char* bmp)
{
	unsigned char blue = bmp[0], green = bmp[1], red = bmp[2];

	if (close(blue, 0) && close(green, 0) && close(red, 0))
		return BLACK;
	else if (close(blue, green) && close(blue, red))
		return WHITE;
	else if (close(blue, red)) {
		if (blue > green) 
			return PURPLE;
		else
			return GREEN;
	}
	else if (close(green, red)) {
		if (green > blue)
			return YELLOW;
		else
			return BLUE;
	}
	else if (close(blue, green)) {
		if (blue > red) 
			return AQUA;
		else
			return RED;
	}
	else {
		int m = imax(imax(blue, green), red);
		if (m == blue) return BLUE;
		else if (m == green) return GREEN;
		else return RED;
	}
		
}

bool close(int x, int y)
{
	return (x > y && (x - DELTA) <= y) || (y > x && (y - DELTA) <= x) || x == y; 
}

int imax(int x, int y)
{
	if (x > y) return x;
	else return y;
}

void print_bmap(unsigned char* buffer)
{
	HANDLE s_out;
	if ((s_out = GetStdHandle(STD_OUTPUT_HANDLE)) != INVALID_HANDLE_VALUE) {
		for (int i = 0; buffer[i] != END_DATA; i++) {
			if (buffer[i] == NEWLINE)
				putchar('\n');
			else {
				SetConsoleTextAttribute(s_out, buffer[i]);
				putchar(254);
			}
		}
	}
	SetConsoleTextAttribute(s_out, WHITE);
	fflush(stdout);
	return;
}

int remove_padding(unsigned char* data, int len)
{
	for (int i = 0; i < len; i++)
		if (data[i] == 0)
			memmove(&(data[i]), &(data[i+1]), --len - i--);
	return len;
}


Until later.

Advertisement

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Theme: WordPress Classic. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.