Skip to content

Instantly share code, notes, and snippets.

@sonOfRa
Created February 12, 2020 13:58
Show Gist options
  • Save sonOfRa/f326b7815face055a569cbd28651bd68 to your computer and use it in GitHub Desktop.
Save sonOfRa/f326b7815face055a569cbd28651bd68 to your computer and use it in GitHub Desktop.
#include <string>
#include <fstream>
#include <sstream>
#include <limits>
#include <iterator>
#include <tuple>
#include <iostream>
#include "ppm.h"
#include "util.h"
ppm::ppm(std::string fileName) : fileName(fileName) {
std::ifstream file(fileName);
if (!file.is_open()) {
throw std::invalid_argument("File " + fileName + " does not exist");
}
std::string line;
std::pair<bool, std::pair<uint64_t, std::string>> error({false, {0, ""}});
state s = state::WANT_P3;
int64_t parse_height, parse_width;
std::vector<size_t> buffer;
while (std::getline(file, line) && !error.first) {
// Increment line counter for error reporting
error.second.first++;
trim(line);
if (line.rfind('#', 0) == 0) {
// Comment, ignore this line
continue;
}
switch (s) {
case state::WANT_P3: {
if (line.length() != 2 || (line.rfind("P3", 0) != 0)) {
error.first = true;
error.second.second = "Invalid file header";
} else {
// Header was successfully parsed as "P3". Next step: read resolution
s = state::WANT_RES;
}
break;
}
case state::WANT_RES: {
std::stringstream ss(line);
// Attempt to read width and height into 32-bit signed integers
if (ss >> parse_width >> parse_height) {
if (parse_width > std::numeric_limits<uint16_t>::max() || parse_width <= 0) {
error.first = true;
error.second.second = "Invalid width, must be 1 <= width <= 2^16 - 1";
} else if (parse_height > std::numeric_limits<uint16_t>::max() || parse_height <= 0) {
error.first = true;
error.second.second = "Invalid height, must be 1 <= width <= 2^16 -1";
} else {
// Resolution successfuly parse within the bounds of uint16_t * uint16_t (around 65536*65536)
width = static_cast<uint16_t>(parse_width);
height = static_cast<uint16_t>(parse_height);
buffer.reserve(width * height);
// After resolution, we want the color depth
s = state::WANT_DEPTH;
}
} else {
// Resolution failed to parse. Probably due to non-numeric characters
error.first = true;
error.second.second = "Invalid resolution";
}
break;
}
case state::WANT_DEPTH: {
if (line.length() != 3 || (line.rfind("255", 0) != 0)) {
error.first = true;
error.second.second = "Color depth must be 255";
} else {
// Color depth is 255, we can now start reading pixel data
s = state::WANT_DATA;
}
break;
}
case state::WANT_DATA: {
// Split up the current row by spaces
std::istringstream iss(line);
std::vector<std::string> words((std::istream_iterator<std::string>(iss)),
std::istream_iterator<std::string>());
if (words.size() % 3 != 0) {
error.first = true;
error.second.second = "Incomplete pixels";
} else {
// We found a multiple of 3 pixel color entries, now try to read them
for (size_t i = 0; i < words.size() && !error.first; i += 3) {
int red = 0;
int green = 0;
int blue = 0;
try {
// Attempt to parse one pixel-triple as integers
red = std::stoi(words[i], nullptr);
green = std::stoi(words[i + 1], nullptr);
blue = std::stoi(words[i + 2], nullptr);
} catch (std::out_of_range &e) {
// Pixel value is out of range for an integer
error.first = true;
error.second.second = "Pixel data out of range";
} catch (std::invalid_argument &e) {
// Pixel value is not an integer
error.first = true;
error.second.second = "Non-numeric pixel data";
}
if (!error.first) {
if (red < 0 || green < 0 || blue < 0) {
error.first = true;
error.second.second = "Negative pixel data not allowed";
} else if (red > std::numeric_limits<uint8_t>::max()
|| green > std::numeric_limits<uint8_t>::max()
|| blue > std::numeric_limits<uint8_t>::max()) {
error.first = true;
error.second.second = "Pixel data > 255 not allowed";
} else {
// Pixel was successfully parsed. Coalesce it into a single matrix-value
auto real_red = static_cast<uint8_t>(red);
auto real_green = static_cast<uint8_t>(green);
auto real_blue = static_cast<uint8_t>(blue);
size_t pixel = real_red;
pixel <<= 8;
pixel |= real_green;
pixel <<= 8;
pixel |= real_blue;
buffer.push_back(pixel);
}
}
}
if (!error.first) {
// If there's no error here, we successfully parsed a line of pixels
size_t dim = width * height;
if (buffer.size() == dim) {
s = state::DONE;
} else if (buffer.size() > dim) {
error.first = true;
error.second.second = "Too many pixels";
}
}
}
break;
}
case state::DONE:
/*
* Getting here means there are non-empty/non-comment lines after the last
* data line. Treat this file as invalid
*/
error.first = true;
error.second.second = "Non-empty line after image fully handled";
break;
}
}
if (error.first) {
throw "Error at line " + std::to_string(error.second.first) + ": " + error.second.second;
}
if (s != state::DONE) {
/*
* Getting here means we parsed all the lines in the file, but less than height.
* This means we are missing pixel data and the file is incomplete
*/
throw "Incomplete file";
}
/*
* Sanity check on buffer size. This should never be true
*/
if (buffer.size() != (size_t) width * height) {
throw "Buffer size does not match width and height. This is a bug.";
}
// Parse was completed sucessfully, allocate matrix with our image dimensions
m = new matrix(buffer.data(), height, width);
}
ppm::~ppm() {
delete m;
}
const matrix *ppm::getMatrix() const {
return m;
}
void ppm::invert() {
std::vector<cl::Platform> all_platforms;
cl::Platform::get(&all_platforms);
if (all_platforms.empty()) {
std::cerr << "No OpenCL platforms found, falling back to CPU" << std::endl;
invert_cpu();
} else {
cl::Platform default_platform = all_platforms[0];
std::vector<cl::Device> all_devices;
default_platform.getDevices(CL_DEVICE_TYPE_ALL, &all_devices);
if (all_devices.empty()) {
std::cerr << "No OpenCL devices found, falling back to CPU" << std::endl;
invert_cpu();
} else {
cl::Device default_device = all_devices[0];
cl::Context context({default_device});
cl::Program::Sources sources;
std::string code(invert_ptx);
sources.push_back({code.c_str(), code.length()});
cl::Program program(context, sources);
if (program.build({default_device}) != CL_SUCCESS) {
throw program.getBuildInfo<CL_PROGRAM_BUILD_LOG>(default_device);
}
cl::Buffer bufA(context, CL_MEM_READ_WRITE, m->getRows() * m->getColumns() * sizeof(size_t));
cl::CommandQueue queue(context, default_device);
queue.enqueueWriteBuffer(bufA, CL_TRUE, 0, m->getRows() * m->getColumns() * sizeof(size_t), m->data);
cl::Kernel invert(program, "invert");
cl_int arg_error = invert.setArg(0, m->getRows());
check_arg_error(arg_error, 0);
arg_error = invert.setArg(1, m->getColumns());
check_arg_error(arg_error, 1);
arg_error = invert.setArg(2, bufA);
check_arg_error(arg_error, 2);
cl_int kernel_err = queue.enqueueNDRangeKernel(invert, cl::NullRange, cl::NDRange(m->getRows()),
cl::NullRange);
check_kernel_error(kernel_err);
queue.enqueueReadBuffer(bufA, CL_TRUE, 0, m->getRows() * m->getColumns() * sizeof(size_t), m->data);
queue.finish();
}
}
}
void ppm::invert_cpu() {
for (size_t i = 0; i < m->getRows(); ++i) {
for (size_t j = 0; j < m->getColumns(); ++j) {
m->data[i * m->getColumns() + j] ^= 0xFFFFFF;
}
}
}
void ppm::writeBack() {
std::ofstream out(fileName);
out << "P3" << std::endl;
out << width << " " << height << std::endl;
out << "255" << std::endl;
for (size_t i = 0; i < m->getRows(); ++i) {
for (size_t j = 0; j < m->getColumns(); ++j) {
size_t index = i * m->getColumns() + j;
size_t data = m->data[index];
auto b = static_cast<uint8_t>(data);
data >>= 8;
auto g = static_cast<uint8_t>(data);
data >>= 8;
auto r = static_cast<uint8_t>(data);
out << +r << " " << +g << " " << +b << " ";
}
out << std::endl;
}
}
bool ppm::operator==(const ppm &rhs) const {
return *m == *rhs.m;
}
bool ppm::operator!=(const ppm &rhs) const {
return *m != *rhs.m;
}
#pragma once
#include <string>
#include "matrix.h"
extern const char invert_ptx[];
/**
* PPM Image class based on matrix.
*
* Every entry in the matrix is one full pixel with RGB data.
*
* Maximum dimensions 65535*65535 pixels
*/
class ppm {
matrix *m;
uint16_t height;
uint16_t width;
public:
/**
* @return the underlying matrix
*/
const matrix *getMatrix() const;
/**
* Invert the image on the CPU. Fallback used if no OpenCL platform or
* device is available
*/
void invert_cpu();
std::string fileName;
/**
* Load and parse the given file
* @param fileName
*/
explicit ppm(std::string fileName);
~ppm();
/**
* Invert the image, that is xor every color of every pixel with 255
*/
void invert();
/**
* Writes the file back to filename, overwriting the file.
*/
void writeBack();
bool operator==(const ppm &rhs) const;
bool operator!=(const ppm &rhs) const;
};
enum class state {
WANT_P3,
WANT_RES,
WANT_DEPTH,
WANT_DATA,
DONE
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment