PNM image loader + tests

This commit is contained in:
Vladyslav Baranovskyi
2024-12-24 21:54:20 +02:00
parent 4d6cba0afd
commit 7ea6edf5d0
3 changed files with 296 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#include "ImageLoaderPnm.hpp"
#include <sstream>
#include <fstream>
namespace OpenVulkano::Image
{
std::unique_ptr<Image> ImageLoaderPnm::loadFromFile(const std::string& filePath)
{
std::ifstream file(filePath, std::ios::binary);
if (!file.is_open())
{
throw std::runtime_error("Failed to open file: " + filePath);
}
std::vector<uint8_t> buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
return loadFromMemory(buffer);
}
std::unique_ptr<Image> ImageLoaderPnm::loadFromMemory(const std::vector<uint8_t>& buffer)
{
std::istringstream stream(std::string(buffer.begin(), buffer.end()));
PnmHeader header = parseHeader(stream);
std::unique_ptr<Image> result = std::make_unique<Image>();
result->resolution.x = header.width;
result->resolution.y = header.height;
result->resolution.z = 1;
if (header.magic == 1 || header.magic == 2 || header.magic == 3)
{
result->data = readTextData(stream, header);
}
else if (header.magic == 4 || header.magic == 5 || header.magic == 6)
{
result->data = readBinaryData(stream, header);
}
else
{
throw std::runtime_error("Unsupported format: P" + (header.magic + '0'));
}
if (header.magic == 1 || header.magic == 2 || header.magic == 4 || header.magic == 5)
{
result->dataFormat = DataFormat::Format::R8_UINT;
}
else
{
result->dataFormat = DataFormat::Format::R8G8B8_UINT;
}
return result;
}
Math::Vector2i ImageLoaderPnm::GetImageDimensions(const std::string& filename)
{
std::ifstream file(filename, std::ios::binary);
if (!file.is_open())
{
throw std::runtime_error("Failed to open file: " + filename);
}
PnmHeader header = parseHeader(file);
return Math::Vector2i(header.width, header.height);
}
PnmHeader ImageLoaderPnm::parseHeader(std::istream& stream)
{
PnmHeader header;
char pMagic;
stream >> pMagic;
if (pMagic != 'P')
{
throw std::runtime_error("Unsupported magic");
}
stream >> header.magic;
header.magic -= '0';
if (header.magic != 1 && header.magic != 2 && header.magic != 3 && header.magic != 4 && header.magic != 5
&& header.magic != 6)
{
throw std::runtime_error("Unsupported magic number: P" + (header.magic + '0'));
}
stream >> header.width >> header.height;
if (header.magic != 1 && header.magic != 4)
{
stream >> header.maxVal;
}
stream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
return header;
}
Array<uint8_t> ImageLoaderPnm::readBinaryData(std::istream& stream, const PnmHeader& header)
{
size_t dataSize = header.width * header.height * ((header.magic == 6) ? 3 : 1);
Array<uint8_t> data(dataSize);
if (header.magic == 4)
{
size_t rowBytes = (header.width + 7) / 8;
Array<uint8_t> rowBuffer(rowBytes);
size_t index = 0;
for (int y = 0; y < header.height; ++y)
{
stream.read(reinterpret_cast<char*>(rowBuffer.Data()), rowBytes);
for (int x = 0; x < header.width; ++x)
{
size_t byteIndex = x / 8;
size_t bitIndex = 7 - (x % 8);
data[index++] = (rowBuffer[byteIndex] >> bitIndex) & 1 ? 255 : 0;
}
}
}
else
{
stream.read(reinterpret_cast<char*>(data.Data()), dataSize);
}
return data;
}
Array<uint8_t> ImageLoaderPnm::readTextData(std::istream& stream, const PnmHeader& header)
{
Array<uint8_t> data(header.width * header.height * ((header.magic == 3) ? 3 : 1));
for (size_t i = 0; i < data.Size(); ++i)
{
int value;
stream >> value;
if (header.magic == 1 || header.magic == 4)
{
data[i] = static_cast<uint8_t>(value * 255);
}
else
{
data[i] = static_cast<uint8_t>((value * 255) / header.maxVal);
}
}
return data;
}
};

View File

@@ -0,0 +1,32 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#pragma once
#include "Image/ImageLoader.hpp"
namespace OpenVulkano::Image
{
struct PnmHeader
{
char magic = 0; // 1 for P1, 2 for P2 etc
int width = 0;
int height = 0;
int maxVal = 255; // Only used for formats P2, P3, P5, P6
};
class ImageLoaderPnm : public IImageLoader
{
public:
std::unique_ptr<Image> loadFromFile(const std::string& filePath) override;
std::unique_ptr<Image> loadFromMemory(const std::vector<uint8_t>& buffer) override;
Math::Vector2i GetImageDimensions(const std::string& filename) override;
private:
PnmHeader parseHeader(std::istream& stream);
Array<uint8_t> readBinaryData(std::istream& stream, const PnmHeader& header);
Array<uint8_t> readTextData(std::istream& stream, const PnmHeader& header);
};
}

View File

@@ -0,0 +1,114 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#include <catch2/catch_test_macros.hpp>
#include "Image/ImageLoaderPnm.hpp"
#include "Scene/DataFormat.hpp"
#include <fstream>
using namespace OpenVulkano;
using namespace OpenVulkano::Image;
TEST_CASE("ImageLoaderPnm - Load P1 (ASCII Black & White)")
{
std::vector<uint8_t> p1Buffer = { 'P', '1', '\n', '4', ' ', '2', '\n', '1', ' ', '0', ' ', '1',
' ', '0', '\n', '0', ' ', '1', ' ', '0', ' ', '1', '\n' };
ImageLoaderPnm loader;
auto image = loader.loadFromMemory(p1Buffer);
REQUIRE(image->resolution.x == 4);
REQUIRE(image->resolution.y == 2);
REQUIRE(image->dataFormat == DataFormat::Format::R8_UINT);
REQUIRE(image->data == Array<uint8_t> { 255, 0, 255, 0, 0, 255, 0, 255 });
}
TEST_CASE("ImageLoaderPnm - Load P2 (ASCII Grayscale)")
{
std::vector<uint8_t> p2Buffer = { 'P', '2', '\n', '4', ' ', '2', '\n', '2', '5', '5', '\n', '0', ' ',
'6', '4', ' ', '1', '2', '8', ' ', '2', '5', '5', '\n', '2', '5',
'5', ' ', '1', '2', '8', ' ', '6', '4', ' ', '0', '\n' };
ImageLoaderPnm loader;
auto image = loader.loadFromMemory(p2Buffer);
REQUIRE(image->resolution.x == 4);
REQUIRE(image->resolution.y == 2);
REQUIRE(image->dataFormat == DataFormat::Format::R8_UINT);
REQUIRE(image->data == Array<uint8_t> { 0, 64, 128, 255, 255, 128, 64, 0 });
}
TEST_CASE("ImageLoaderPnm - Load P3 (ASCII RGB)")
{
std::vector<uint8_t> p3Buffer = { 'P', '3', '\n', '2', ' ', '2', '\n', '2', '5', '5', '\n', '2', '5', '5', ' ', '0',
' ', '0', ' ', '0', ' ', '2', '5', '5', ' ', '0', '\n', '0', ' ', '0', ' ', '2',
'5', '5', ' ', '2', '5', '5', ' ', '2', '5', '5', ' ', '2', '5', '5', '\n' };
ImageLoaderPnm loader;
auto image = loader.loadFromMemory(p3Buffer);
REQUIRE(image->resolution.x == 2);
REQUIRE(image->resolution.y == 2);
REQUIRE(image->dataFormat == DataFormat::Format::R8G8B8_UINT);
REQUIRE(image->data == Array<uint8_t> { 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255 });
}
TEST_CASE("ImageLoaderPnm - Load P4 (Binary Black & White)")
{
std::vector<uint8_t> p4Buffer = { 'P', '4', '\n', '8', ' ', '2', '\n', 0b10101010, 0b01010101 };
ImageLoaderPnm loader;
auto image = loader.loadFromMemory(p4Buffer);
REQUIRE(image->resolution.x == 8);
REQUIRE(image->resolution.y == 2);
REQUIRE(image->dataFormat == DataFormat::Format::R8_UINT);
REQUIRE(image->data
== Array<uint8_t> {
255, 0, 255, 0, 255, 0, 255, 0, // Row 1
0, 255, 0, 255, 0, 255, 0, 255 // Row 2
});
}
TEST_CASE("ImageLoaderPnm - Load P5 (Binary Grayscale)")
{
std::vector<uint8_t> p5Buffer = { 'P', '5', '\n', '4', ' ', '2', '\n', '2', '5', '5',
'\n', 0, 64, 128, 255, 255, 128, 64, 0 };
ImageLoaderPnm loader;
auto image = loader.loadFromMemory(p5Buffer);
REQUIRE(image->resolution.x == 4);
REQUIRE(image->resolution.y == 2);
REQUIRE(image->dataFormat == DataFormat::Format::R8_UINT);
REQUIRE(image->data == Array<uint8_t> { 0, 64, 128, 255, 255, 128, 64, 0 });
}
TEST_CASE("ImageLoaderPnm - Load P6 (Binary RGB)")
{
std::vector<uint8_t> p6Buffer = { 'P', '6', '\n', '2', ' ', '2', '\n', '2', '5', '5', '\n', 255,
0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255 };
ImageLoaderPnm loader;
auto image = loader.loadFromMemory(p6Buffer);
REQUIRE(image->resolution.x == 2);
REQUIRE(image->resolution.y == 2);
REQUIRE(image->dataFormat == DataFormat::Format::R8G8B8_UINT);
REQUIRE(image->data == Array<uint8_t> { 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255 });
}
TEST_CASE("ImageLoaderPnm - Get Dimensions")
{
ImageLoaderPnm loader;
std::string mockFile = "mock.pnm";
std::ofstream outFile(mockFile);
outFile << "P6\n2 2\n255\n";
outFile.close();
auto dimensions = loader.GetImageDimensions(mockFile);
REQUIRE(dimensions.x == 2);
REQUIRE(dimensions.y == 2);
std::remove(mockFile.c_str());
}