Skip to content

Instantly share code, notes, and snippets.

@tjb0607
Last active December 11, 2017 18:17
Show Gist options
  • Save tjb0607/e3c012a3df7f33d9d8ba to your computer and use it in GitHub Desktop.
Save tjb0607/e3c012a3df7f33d9d8ba to your computer and use it in GitHub Desktop.
/********************************************************************************************
*
* Author: Tyler Beatty
* Date Created: 2015-03-02
* Last modified: 2015-03-04
* Lab number CST 116 Final Project
* Filename bdf-cli-render.cpp
*
* Overview:
* A BDF font is loaded into a Font class containing an array of 256 ASCII characters,
* each containing a binary bitmap, stored as an array of numbers. Once loaded, it will
* get a string from cin (or command line args) and place each character one at a time
* in a binary bitmap containing the current line of text, stored as an array of bitsets.
* Once the current line of text has no room left for another character, it will print out
* and clear the bitmap to move onto the next line. The bitmap is printed out two rows at a
* time using these characters in a single line of text: ▄, █, ▀. Also, the code is fully
* portable and handles command line arguments very well.
*
* Input:
* Options (see output of --help), a bitmap font file in the BDF format, a string to render.
*
* Output:
* The given string will be rendered in the given font to the command line, such that each
* character contains two pixels.
*
* Example output:
*
* tyler@desktop ~/programming/bdf-cli-render
* % ./bdf-cli-render
* Enter font directory [default: fonts/Tewi.bdf]:
* Enter string to render: Hello, world!
*
* █ █ ▀█ ▀█ ▀█ █ █
* █▄▄▄█ ▄▀▀▀▄ █ █ ▄▀▀▀▄ █ ▄ █ ▄▀▀▀▄ █▄▀▀▄ █ ▄▀▀▀█ █
* █ █ █▀▀▀▀ █ █ █ █ ▄▄ █ █ █ █ █ █ █ █ █ ▀
* ▀ ▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀ ▀█ ▀ ▀ ▀▀▀ ▀ ▀▀ ▀▀▀▀ ▀
* ▀
* Enter string to render: █
*
********************************************************************************************/
#include <iostream>
#include <iomanip>
#include <fstream>
#include <cstring>
#include <sstream>
#include <new>
#include <bitset> // todo: replace bitsets with dynamic bitsets from Boost. Putting this off until after I turn it in so that it'll run on the lab computers without installing extra stuff.
#include <stdexcept>
using namespace std;
const int STRING_SIZE = 256;
#ifdef _WIN32
const char FULL_BLOCK[] = { (char)(unsigned char)219, '\0' }; // double typecast to prevent compiler warnings
const char UPPER_HALF_BLOCK[] = { (char)(unsigned char)223, '\0' };
const char LOWER_HALF_BLOCK[] = { (char)(unsigned char)220, '\0' };
const char DEFAULT_FONT[] = "fonts\\Tewi.bdf";
#else
const char FULL_BLOCK[] = "\u2588"; // unicode
const char UPPER_HALF_BLOCK[] = "\u2580";
const char LOWER_HALF_BLOCK[] = "\u2584";
const char DEFAULT_FONT[] = "fonts/Tewi.bdf";
#endif
const int TERMINAL_WIDTH = 80;
class Character {
public:
unsigned int* bitmap; // use a short array for the bitmap because a bitset's size must be known during compile
short dwidth;
short bbxWidth;
short bbxHeight;
short bbxOffsetX;
short bbxOffsetY;
short bitsPerRow;
};
class Font {
private:
void InitBitmaps();
bool LoadCharacter(ifstream& fontFile);
short ReadBitmap(ifstream& fontFile, unsigned int* bitmap);
public:
void DeleteBitmaps();
bool Load(ifstream &fontFile);
short bbxWidth;
short bbxHeight;
short bbxOffsetX;
short bbxOffsetY;
short ascent;
short descent;
short bitsPerRow; // of character's bitmap
Character chars[256];
};
class FontRenderer {
private:
bool ParseArgs(int argc, char* argv[], char* stringToPrint, int stpLen);
bool PutChar(Font font, char myChar);
void PrintBitmap();
public:
bool SetVars(ifstream& fontFile, int argc, char* argv[], char* stringToPrint, int stpLen); // return false if the program should exit
void InitBitmap(Font font);
void DeleteBitmap();
void RenderString(Font font, char* stringToPrint);
void ClearBitmap();
bitset<TERMINAL_WIDTH>* bitmap;
char* fontDir;
bool monospaceMode; // allow more than one pixel of padding on the left
bool compactMode; // only print non-empty lines
bool fullMode; // use bbxHeight instead of ascent + descent
bool listen;
short bmpHeight;
short bmpOffsetY;
short posX;
};
void PrintHelp()
{
cout << left;
cout << "Usage: bdf-cli-render [OPTION] [MESSAGE]" << endl;
cout << "Render message to CLI using BDF font" << endl;
cout << setw(6) << " -f, " << setw(20) << "--font=FILE" << "specify font" << endl;
cout << setw(6) << " -c, " << setw(20) << "--compact" << "compact mode (don't print empty lines)" << endl;
cout << setw(6) << " -m, " << setw(20) << "--monospace" << "monospace mode (allow padding of leftmost char)" << endl;
cout << setw(6) << " -F, " << setw(20) << "--full" << "use the full bounding box height instead of the" << endl;
cout << setw(26) << " " << "font height (useful for special symbols)" << endl;
cout << setw(6) << " -h, " << setw(20) << "--help" << "print this message and quit" << endl;
}
// String functions because visual studio's compiler complains and I don't feel like importing another lib for strcpy_s, strcat_s
bool StrCpy(char* destination, const char* source, int num)
{
int i = 0;
while (source[i] != '\0' && i < num)
{
destination[i] = source[i];
i++;
}
if (i == num && source[i - 1] == '\0')
return false;
else
return true;
}
// returns whether successful
bool StrCat(char* destination, const char* source, int num)
{
int destLen = strlen(destination);
return StrCpy(destination + destLen, source, num - destLen);
}
bool StrJoin(char* destination, const char* source, int num, char joinChar)
{
int destLen = strlen(destination);
if ( num > destLen )
{
if ( destLen )
{
*(destination + destLen) = joinChar;
return StrCpy(destination + destLen + 1, source, num - destLen - 1);
}
else
{
return StrCpy(destination, source, num);
}
}
else
{
return false;
}
}
// returns the length of str2 if the first sequence of characters in str1 matches str2, else returns 0
int StrStartsWith(const char* str1, const char* str2)
{
int i = 0;
while (str2[i] != '\0')
{
if (str2[i] != str1[i])
return 0;
i++;
}
return i;
}
// read up to '\n' or '\0', return false if ends with '\0', silently truncate to stringSize
bool GetLine(istream &inputStream, char outputString[], int stringSize)
{
char currentChar;
int stringIndex = 0;
inputStream.get(currentChar);
while (currentChar != '\n' && currentChar != '\0' &&
(stringIndex + 1) < stringSize)
{
outputString[stringIndex] = currentChar;
inputStream.get(currentChar);
stringIndex++;
}
outputString[stringIndex] = '\0';
return currentChar != '\0';
}
// read up to ' ', '\n', '\0', or stringSize; return the char that terminated the read
char GetWord(ifstream &fontFile, char string[], int stringSize)
{
char currentChar;
int stringIndex = 0;
fontFile.get(currentChar);
while (currentChar != '\n' && currentChar != ' ' && currentChar != '\0' &&
(stringIndex + 1) < stringSize)
{
string[stringIndex] = currentChar;
fontFile.get(currentChar);
stringIndex++;
}
string[stringIndex] = '\0';
return currentChar;
}
// move to the properties of a found string, return the pointer to the string found
char* MoveToNext(ifstream &fontFile, const char strings[5][32], int numStrings)
{
char currentWord[STRING_SIZE];
char currentChar = '\n';
char* foundItem = nullptr;
do
{
if (currentChar != '\n')
fontFile.ignore(STRING_SIZE, '\n'); // ignore characters until next line
currentChar = GetWord(fontFile, currentWord, STRING_SIZE);
if (currentChar == '\r')
currentChar = GetWord(fontFile, currentWord, STRING_SIZE); // handle DOS-style newlines
for (int i = 0; i < numStrings; i++)
{
if (strcmp(strings[i], currentWord) == 0)
{
foundItem = (char *)strings[i]; // the string it found
}
}
} while (foundItem == nullptr && !fontFile.eof());
return foundItem;
}
int CalcBitsPerRow(short bbxWidth)
{
return (bbxWidth + 7) & ~7; // adds 1 bit so that it will always round up, then truncates the last 3 binary digits to make it divisible by 8
}
void Font::InitBitmaps()
{
for (int i = 0; i < 256; i++)
{
chars[i].bitmap = new unsigned int[bbxHeight];
}
}
void Font::DeleteBitmaps()
{
for (int i = 0; i < 256; i++)
{
delete[] chars[i].bitmap;
}
}
// takes a list of 2-digit hex numbers and converts it to a bool matrix, returns the bits per row
short Font::ReadBitmap(ifstream &fontFile, unsigned int* bitmap)
{
char currentWord[STRING_SIZE];
int i = 0;
int bitsPerRow = 0;
GetWord(fontFile, currentWord, STRING_SIZE);
while (strcmp(currentWord, "ENDCHAR"))
{
if (!i)
bitsPerRow = strlen(currentWord) * 4;
stringstream tmp; // temporary stringstream for converting hex to a number
tmp << hex << currentWord;
tmp >> bitmap[i]; // bitmap[i] now stores the bits of the bitmap's row as a number, so that when converted to binary, 0 is a pixel, 1 is no pixel
GetWord(fontFile, currentWord, STRING_SIZE);
i++;
}
return bitsPerRow;
}
bool Font::LoadCharacter(ifstream &fontFile)
{
const char bdfCharStrings[5][32] = {
"ENCODING",
"DWIDTH",
"BBX",
"BITMAP",
"ENDCHAR"
};
short charEncoding;
bool endOfChar = false;
while (!endOfChar)
{
char* foundItem;
foundItem = MoveToNext(fontFile, bdfCharStrings, 5);
if (foundItem == nullptr)
{
cout << "ERROR: Invalid BDF file." << endl;
return false;
}
// convert pointer to index of pointer in bdfMetaStrings
int foundItemIndex = 0;
while (foundItem != bdfCharStrings[foundItemIndex])
{
foundItemIndex++;
if (foundItemIndex >= 5)
{
cout << "Internal error: bad pointer" << endl;
return false;
}
}
switch (foundItemIndex)
{
case 0: // ENCODING
fontFile >> charEncoding;
if (charEncoding > 255)
return false;
break;
case 1: // DWIDTH
fontFile >> chars[charEncoding].dwidth;
break;
case 2: // BBX
fontFile >> chars[charEncoding].bbxWidth;
fontFile >> chars[charEncoding].bbxHeight;
fontFile >> chars[charEncoding].bbxOffsetX;
fontFile >> chars[charEncoding].bbxOffsetY;
break;
case 3: // BITMAP
chars[charEncoding].bitsPerRow = ReadBitmap(fontFile, chars[charEncoding].bitmap);
endOfChar = true;
break;
//case 4: // ENDCHAR (this shouldn't happen)
// endOfChar = true;
// break;
}
}
return true;
}
// returns whether or not loading the font was successful
bool Font::Load(ifstream &fontFile)
{
const char bdfStartString[5][32] = {
"STARTFONT",
"", "", "", ""
};
if (MoveToNext(fontFile, bdfStartString, 1) == nullptr)
{
cout << "ERROR: Not a BDF file." << endl;
throw;
}
const char bdfStrings[5][32] = {
"FONTBOUNDINGBOX",
"FONT_ASCENT",
"FONT_DESCENT",
"STARTCHAR",
"ENDFONT"
};
bool done = false;
while (!done)
{
char* foundItem;
foundItem = MoveToNext(fontFile, bdfStrings, 5);
if (foundItem == nullptr)
{
cout << "ERROR: Invalid BDF file." << endl;
return false;
}
// convert pointer to index of pointer in bdfMetaStrings
int foundItemIndex = 0;
while (foundItem != bdfStrings[foundItemIndex])
{
foundItemIndex++;
if (foundItemIndex >= 5)
{
cout << "Internal error: bad pointer" << endl;
return false;
}
}
switch (foundItemIndex)
{
case 0: // FONTBOUNDINGBOX
fontFile >> bbxWidth;
fontFile >> bbxHeight;
fontFile >> bbxOffsetX;
fontFile >> bbxOffsetY;
bitsPerRow = CalcBitsPerRow(bbxWidth);
InitBitmaps();
break;
case 1: // FONT_ASCENT
fontFile >> ascent;
break;
case 2: // FONT_DESCENT
fontFile >> descent;
break;
case 3: // STARTCHAR
if (!LoadCharacter(fontFile))
done = true;
break;
case 4:
done = true;
break;
}
if (fontFile.eof())
done = true;
}
return true;
}
// places the character on the bitmap and returns true iff there's enough room
bool FontRenderer::PutChar(Font font, char currentChar)
{
Character myCharacter = font.chars[(unsigned char)currentChar];
// coordinates of top right corner of boundary box
if (posX == 0)
{
if(!monospaceMode)
posX = -myCharacter.bbxOffsetX - font.bbxOffsetX;
else
posX = -font.bbxOffsetX;
}
int topRightX = posX + myCharacter.bbxOffsetX + font.bbxOffsetX + myCharacter.bitsPerRow;
int topRightY = bmpOffsetY + font.bbxOffsetY - myCharacter.bbxOffsetY - myCharacter.bbxHeight + font.bbxHeight;
if (topRightX >= TERMINAL_WIDTH)
return false; // render bitmap & move to next line
for (int charbmpY = 0; charbmpY < myCharacter.bbxHeight; charbmpY++)
{
unsigned short charbmpRow = myCharacter.bitmap[charbmpY];
for (int charbmpX = 0; charbmpX < font.bitsPerRow; charbmpX++)
{
if (charbmpRow % 2)
{
//coordinates of the current pixel to be placed
int pxposX = topRightX - charbmpX;
int pxposY = charbmpY + topRightY;
if (pxposX >= 0 && pxposX < TERMINAL_WIDTH &&
pxposY >= 0 && pxposY < bmpHeight) // assert that the pixel being written is in bounds
bitmap[pxposY].set(pxposX, 1);
}
charbmpRow /= 2;
}
}
posX += myCharacter.dwidth;
return true;
}
// prints out the bool matrix
void FontRenderer::PrintBitmap()
{
for (int i = 0; i < bmpHeight; i += 2)
{
if ( !compactMode || !bitmap[i].none() || !bitmap[i+1].none() ) // compactMode doesn't print blank lines
{
for (int j = 0; j < TERMINAL_WIDTH; j++)
{
if (bitmap[i][j] == 1)
{
if (bitmap[i + 1][j] == 1)
{
cout << FULL_BLOCK;
}
else
{
cout << UPPER_HALF_BLOCK;
}
}
else
{
if (bitmap[i + 1][j] == 1)
{
cout << LOWER_HALF_BLOCK;
}
else
{
cout << ' ';
//cout << "\u2591";
}
}
bitmap[i].reset(j);
bitmap[i + 1].reset(j);
if (bitmap[i].none() && bitmap[i + 1].none())
break; // break from loop if the rest of the characters in the current line are spaces
}
cout << endl;
}
}
}
// clear the bitmap for the next line
void FontRenderer::ClearBitmap()
{
for (int i = 0; i < bmpHeight; i++)
{
bitmap[i].reset();
}
return;
}
void FontRenderer::InitBitmap(Font font)
{
if ( !fullMode )
{
bmpHeight = font.ascent + font.descent;
bmpOffsetY = font.ascent - font.bbxHeight - font.bbxOffsetY;
}
else
{
bmpHeight = font.bbxHeight;
bmpOffsetY = 0;
}
if (bmpHeight % 2)
bmpHeight++; //make bmpHeight even
bitmap = new bitset<TERMINAL_WIDTH>[bmpHeight]; // bitmap image containing the pixels for the current text line
ClearBitmap();
}
void FontRenderer::DeleteBitmap()
{
delete[] bitmap;
}
// main function for printing out a string
void FontRenderer::RenderString(Font font, char* stringToPrint)
{
int i = 0;
while (stringToPrint[i] != '\0')
{
if ( stringToPrint[i] == '\n' )
{
PrintBitmap();
posX = 0;
}
else if ( stringToPrint[i] == '\\' && stringToPrint[i+1] == 'n' ) // "\n" becomes newline
{
PrintBitmap();
posX = 0;
i++;
}
else
{
if ( stringToPrint[i] == '\\' ) // allow for "\\n" to make literal "\n"
i++;
while ( !PutChar(font, stringToPrint[i]) ) // put the current character
{
PrintBitmap(); // if there wasn't enough room, start a new line
posX = 0;
}
}
i++;
}
PrintBitmap(); // print out the current line
return;
}
// huge mess
bool FontRenderer::ParseArgs(int argc, char* argv[], char* stringToPrint, int stpLen)
{
int argi = 1; // start at 1 to ignore executable location
int argi_increment = 1;
while ( argi < argc )
{
if ( argv[argi][0] == '-' )
{
// handle --args as full words but -args as individual letters
if ( argv[argi][1] == '-' )
{
if ( strcmp( argv[argi] + 2, "compact" ) == 0 )
{
compactMode = true;
fullMode = true; // don't cut off any characters
}
else if ( StrStartsWith( argv[argi] + 2, "font=" ) )
{
if ( argv[argi] + 7 != '\0' )
{
fontDir = argv[argi] + 7;
}
else
{
throw invalid_argument("No font given");
}
}
else if ( strcmp( argv[argi] + 2, "font" ) == 0 )
{
if ( argc > argi + 1 )
{
fontDir = argv[argi+1];
argi_increment = 2;
}
else
{
throw invalid_argument("No font given");
}
}
else if ( strcmp( argv[argi] + 2, "help" ) == 0 )
{
PrintHelp();
return false;
}
else if ( strcmp( argv[argi] + 2, "monospace" ) == 0 )
{
monospaceMode = true;
}
else if ( strcmp( argv[argi] + 2, "full" ) == 0 )
{
fullMode = true;
}
else
{
StrJoin(stringToPrint, argv[argi], stpLen, ' ');
listen = false;
}
}
else if ( argv[argi][1] != '\0' )
{
int j = 1;
while ( argv[argi][j] != '\0' && j < 10 )
{
switch ( argv[argi][j] )
{
case 'f':
if ( argc > argi + 1 )
{
fontDir = argv[argi+1];
argi_increment += 1;
}
else
{
throw invalid_argument("No font given");
}
break;
case 'm':
monospaceMode = true;
break;
case 'F':
fullMode = true;
break;
case 'c':
compactMode = true;
fullMode = true; // don't cut off any characters
break;
case 'h':
case '?':
PrintHelp();
return false;
break;
default:
StrJoin(stringToPrint, argv[argi], stpLen, ' ');
listen = false;
break;
}
j += 1;
}
}
else
{
StrJoin(stringToPrint, argv[argi], stpLen, ' ');
listen = false;
}
}
else
{
StrJoin(stringToPrint, argv[argi], stpLen, ' ');
listen = false;
}
argi += argi_increment;
argi_increment = 1;
}
}
bool FontRenderer::SetVars(ifstream& fontFile, int argc, char* argv[], char* stringToPrint, int stpLen)
{
monospaceMode = false;
fullMode = false;
compactMode = false;
listen = true;
posX = 0;
if ( ParseArgs(argc, argv, stringToPrint, stpLen) )
{
if ( fontDir != nullptr )
{
fontFile.open(fontDir);
if ( !fontFile.is_open() )
{
char errorMsg[STRING_SIZE] = "Unable to open file: ";
StrCat(errorMsg, fontDir, STRING_SIZE);
throw invalid_argument(errorMsg);
}
}
else
{
while ( !fontFile.is_open() )
{
cout << "Enter font BDF filepath [default: " << DEFAULT_FONT << "]: ";
char fontDir[STRING_SIZE];
cin.getline(fontDir, STRING_SIZE);
if (fontDir[0] == '\0') //default
fontFile.open(DEFAULT_FONT);
else
fontFile.open(fontDir);
}
}
return true;
}
else
{
return false;
}
}
int main(int argc, char* argv[]) // input handling mostly
{
char* stringToPrint = new char[STRING_SIZE];
stringToPrint[0] = '\0';
ifstream fontFile;
FontRenderer renderer;
if ( renderer.SetVars(fontFile, argc, argv, stringToPrint, STRING_SIZE) )
{
Font font;
font.Load(fontFile);
fontFile.close();
renderer.InitBitmap(font);
if ( argc == 1 )
cout << "Enter string to render: ";
if ( renderer.listen )
{
while ( GetLine(cin, stringToPrint, STRING_SIZE) )
{
renderer.RenderString(font, stringToPrint);
renderer.ClearBitmap();
renderer.posX = 0;
if ( argc == 1 )
cout << "Enter string to render: ";
}
}
else
{
renderer.RenderString(font, stringToPrint);
}
renderer.DeleteBitmap();
font.DeleteBitmaps();
}
delete[] stringToPrint;
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment