/*
PROJECT: Terminal-Based PPM Image Renderer
ABOUT:
This is a fully self-contained PPM (Portable Pixmap) image renderer
that runs in the terminal
FEATURES:
Renders PPM (P3) image files
REAL WORLD APPLICATION:
This program can be used to view and display images directly in the terminal.
*/
#include
#include
#include
#include
#include
#include
#include
// if we are on Windows include the Windows API header
// We need this because Windows' terminal for backwards compatibility reasons is ASCII by default
// And because we are using Unicode we have to tell it that
#ifdef _WIN32
#include
#endif
// Each pixel will be rendered using this block character.
// Using two characters makes the image look more square in the terminal.
#define PIXEL "██"
// Test image stored as a raw string literal.
// This allows testing the parser without loading a file.
#define TEST_FILE R"(P3
# Example PPM file for testing
4 4 255
255 0 0
255 0 255
0 255 0
0 255 255
128 128 128
255 128 0
64 64 64
0 0 255
192 192 192
255 64 64
255 255 0
255 255 255
0 0 0
0 128 255
128 0 255
64 255 64
)"
// A Pixel stores three integer values in this order: { Red, Green, Blue }
using Pixel = std::array;
// The Image structure holds all parsed data from a PPM file.
// width and height define the image size.
// max_value defines the maximum color intensity (usually 255).
// pixels stores all RGB values in row-major order.
struct Image {
int width = 0;
int height = 0;
int max_value = 255;
std::vector pixels;
};
/*
This is an internal helper to load files into a `std::string` object
*/
std::string _LoadFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
std::cerr << "Error opening file: " << filename << std::endl;
return ""; // Return empty string
}
std::stringstream buffer;
buffer << file.rdbuf(); // Read the file's stream buffer into the stringstream
file.close(); // Close the file stream
return buffer.str(); // Convert the stringstream to a std::string
}
/*
This function reads a P3 formatted PPM image from a string
and fills the Image structure with parsed data.
Returns:
true -> if parsing succeeded
false -> if any errors occurred
*/
bool ParsePPM(const std::string& data, Image& img) {
std::istringstream stream(data);
std::string token;
// Read and verify the magic number (must be P3 for ASCII PPM)
stream >> token;
if (token != "P3") { return false; }
// Read width
// If a comment is encountered (starts with #),
// skip the rest of that line and continue.
while (true) {
stream >> token;
if (token.size() > 0 && token[0] == '#') {
std::getline(stream, token);
continue;
}
img.width = std::stoi(token);
break;
}
// Read height
stream >> img.height;
// Read maximum color value (usually 255)
stream >> img.max_value;
if (img.width <= 0 || img.height <= 0) return false;
// Validate dimensions to prevent invalid or corrupted input
int total_pixels = img.width * img.height;
// Clear any previous image data
img.pixels.clear();
// Read every pixel in order.
// Each pixel consists of 3 integers: R, G and B
for (int i = 0; i < total_pixels; i++) {
Pixel p;
if (!(stream >> p[0])) return false;
if (!(stream >> p[1])) return false;
if (!(stream >> p[2])) return false;
img.pixels.push_back(p);
}
return true;
}
/*
Prints a single pixel to the terminal using
ANSI 24-bit background color escape codes.
*/
void DrawPixel(const Pixel& pixel) {
int r = pixel[0];
int g = pixel[1];
int b = pixel[2];
// Set background color
std::cout << "\x1b[38;2;"
<< r << ";"
<< g << ";"
<< b << "m"
<< PIXEL;
// Reset formatting so colors don’t bleed
std::cout << "\x1b[0m";
}
/*
This function will take the parsed image data and print it
to the terminal using ANSI escape codes for 24-bit color.
Each pixel is drawn as a colored block.
*/
void RenderImage(const Image& image) {
for (int y = 0; y < image.height; y++) {
for (int x = 0; x < image.width; x++) {
int index = y * image.width + x;
DrawPixel(image.pixels[index]);
}
// Move to next line after each row
std::cout << "\n";
}
}
/*
This function parses the TEST_FILE and renders it.
It allows testing without loading from disk.
*/
void RenderTestImage() {
Image img;
bool ok = ParsePPM(TEST_FILE, img);
if (!ok) {
std::cerr << "Error parsing test image\n";
std::exit(-1);
}
RenderImage(img);
}
enum Mode {
NORMAL,
TEST,
};
/*
Determines which mode the program should run in
based on command-line arguments.
--test -> TEST mode
(none) -> NORMAL mode
*/
Mode GetMode(int argc, char** argv) {
// Default mode
Mode mode = NORMAL;
// If at least one argument was provided
if (argc > 1) {
std::string arg = argv[1];
if (arg == "--test") {
mode = TEST;
}
}
return mode;
}
int main(int argc, char** argv) {
#ifdef _WIN32
// Tell Windows we want the terminal to use Unicode Transformation Format 8bit
SetConsoleOutputCP(CP_UTF8);
#endif
Mode mode = GetMode(argc, argv);
switch (mode) {
case NORMAL: {
if (argc < 2) {
std::cerr << "Usage: program \n";
return -1;
}
// It didn't error so we can now load and render the PPM file
std::string file_data = _LoadFile(argv[1]);
Image img;
ParsePPM(file_data, img);
RenderImage(img);
break;
}
case TEST: {
// Render the test image to the terminal
RenderTestImage();
break;
}
}
return 0;
}