Skip to content

Instantly share code, notes, and snippets.

@tjb0607
Created March 4, 2015 15:41
Show Gist options
  • Save tjb0607/dad0d141ec280751feb8 to your computer and use it in GitHub Desktop.
Save tjb0607/dad0d141ec280751feb8 to your computer and use it in GitHub Desktop.
/*
MAKE FLAGS: g++ -std=gnu++0x ./bdf-cli-render.cpp
Name Tyler Beatty
Started 2015-03-02
Last modified 2015-03-04
Final Project
bdf-cli-render.cpp
input:
command line args: ./bdf-cli-render [<font> [string to render]]
if neither are given, the user will be prompted to enter the font (or go with the default).
if just a font is given, it will be in quiet mode where stdout will only have the rendered glyphs.
if both a font and a string are given, it will run in quiet mode and render only the given string, ignoring stdin.
the program will load a font in the Glyph Bitmap Distribution Format (BDF).
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 <fstream>
#include <cstring>
#include <sstream>
#include <new>
#include <bitset>
using namespace std;
#ifdef _WIN32
const char FULL_BLOCK[] = { (char)(unsigned char)219, '\0' } // double typecast to prevent g++ 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[] = "/usr/share/fonts/misc/Tewi-normal-11.bdf"; // changed to full directory for ruukasu
#endif
const int TERMINAL_WIDTH = 80;
//const int CHAR_SIZE_X = 8; // I'd rather not use constants here but doing otherwise would mean major restructuring
//const int CHAR_SIZE_Y = 16;
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];
};
// 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[128];
char currentChar = '\n';
char* foundItem = nullptr;
do
{
if (currentChar != '\n')
fontFile.ignore(128, '\n'); // ignore characters until next line
currentChar = GetWord(fontFile, currentWord, 128);
if (currentChar == '\r')
currentChar = GetWord(fontFile, currentWord, 128); // 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[128];
int i = 0;
int bitsPerRow = 0;
GetWord(fontFile, currentWord, 128);
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, 128);
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
cout << "gotcha" << endl;
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 PutChar(Font myFont, char myChar, int& posX, bitset<TERMINAL_WIDTH>* bitmap, short bmpHeight)
{
Character myCharacter = myFont.chars[(unsigned char)myChar];
if (posX + myCharacter.dwidth >= TERMINAL_WIDTH)
return false;
// coordinates of top right corner of boundary box
int topRightX = posX + myCharacter.bbxOffsetX + myFont.bbxOffsetX + myCharacter.bitsPerRow + 2;
int topRightY = myFont.bbxOffsetY - myCharacter.bbxOffsetY - myCharacter.bbxHeight + myFont.bbxHeight;
for (int charbmpY = 0; charbmpY < myCharacter.bbxHeight; charbmpY++)
{
unsigned short charbmpRow = myCharacter.bitmap[charbmpY];
for (int charbmpX = 0; charbmpX < myFont.bitsPerRow; charbmpX++)
{
// cout << "charbmpX " << charbmpX << endl;
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 PrintBitmap(bitset<TERMINAL_WIDTH>* bitmap, short bmpHeight)
{
for (int i = 0; i < bmpHeight; i += 2)
{
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 << ' ';
}
}
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 ClearBitmap(bitset<TERMINAL_WIDTH>* bitmap, short bmpHeight)
{
for (int i = 0; i < bmpHeight; i++)
{
bitmap[i].reset();
}
return;
}
// main function for printing out a string
void RenderString(Font myFont, bitset<TERMINAL_WIDTH>* bitmap, short bmpHeight, char const * stringToPrint, int& posX)
{
int i = 0;
while (stringToPrint[i] != '\0')
{
while ( !(PutChar(myFont, stringToPrint[i], posX, bitmap, bmpHeight)) ) // put the current character
{
PrintBitmap(bitmap, bmpHeight); // if there wasn't enough room start a new line
posX = 0;
}
i++;
}
PrintBitmap(bitmap, bmpHeight); // print out the current line
return;
}
int main(int argc, char* argv[]) // input handling mostly
{
ifstream fontFile;
// open the font
if (argc > 1)
{
fontFile.open(argv[1]);
}
while ( !fontFile.is_open() )
{
cout << "Enter font directory [default: " << DEFAULT_FONT << "]: ";
char fontDir[256];
cin.getline(fontDir, 256);
if (fontDir[0] == '\0') // default
fontFile.open(DEFAULT_FONT);
else
fontFile.open(fontDir);
}
Font myFont;
myFont.Load(fontFile);
fontFile.close();
short bmpHeight = myFont.bbxHeight;
if ( bmpHeight % 2 )
bmpHeight++;
bitset<TERMINAL_WIDTH> bitmap[bmpHeight]; // bitmap image to contain the current line of text
ClearBitmap(bitmap, bmpHeight);
int posX = 0;
char stringToPrint[512];
if (argc > 2)
{
strcpy(stringToPrint, argv[2]); // get string from args after the bdf file
for(int i = 3; i < argc; i++)
{
strcat(stringToPrint, " ");
strcat(stringToPrint, argv[i]);
}
}
else
{
if (argc == 1)
cout << "Enter string to render: ";
if (!GetLine(cin, stringToPrint, 512))
return 0;
}
RenderString(myFont, bitmap, bmpHeight, stringToPrint, posX);
while (argc <= 2) // if the string wasn't declared from command line args, accept input forever
{
if (argc == 1)
cout << "Enter string to render: ";
ClearBitmap(bitmap, bmpHeight);
posX = 0;
if (!GetLine(cin, stringToPrint, 512)) // can happen when being piped through stdin, for example "$ echo "Hello World" | ./bdf-cli-render ./Tewi.bdf"
return 0;
RenderString(myFont, bitmap, bmpHeight, stringToPrint, posX);
}
myFont.DeleteBitmaps();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment