diff --git a/openVulkanoCpp/Image/ImageLoaderPnm.cpp b/openVulkanoCpp/Image/ImageLoaderPnm.cpp new file mode 100644 index 0000000..a21fcc3 --- /dev/null +++ b/openVulkanoCpp/Image/ImageLoaderPnm.cpp @@ -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 +#include + +namespace OpenVulkano::Image +{ + std::unique_ptr 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 buffer((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + return loadFromMemory(buffer); + } + + std::unique_ptr ImageLoaderPnm::loadFromMemory(const std::vector& buffer) + { + std::istringstream stream(std::string(buffer.begin(), buffer.end())); + PnmHeader header = parseHeader(stream); + + std::unique_ptr result = std::make_unique(); + 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::max(), '\n'); + return header; + } + + Array ImageLoaderPnm::readBinaryData(std::istream& stream, const PnmHeader& header) + { + size_t dataSize = header.width * header.height * ((header.magic == 6) ? 3 : 1); + Array data(dataSize); + + if (header.magic == 4) + { + size_t rowBytes = (header.width + 7) / 8; + Array rowBuffer(rowBytes); + size_t index = 0; + + for (int y = 0; y < header.height; ++y) + { + stream.read(reinterpret_cast(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(data.Data()), dataSize); + } + + return data; + } + + Array ImageLoaderPnm::readTextData(std::istream& stream, const PnmHeader& header) + { + Array 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(value * 255); + } + else + { + data[i] = static_cast((value * 255) / header.maxVal); + } + } + return data; + } +}; \ No newline at end of file diff --git a/openVulkanoCpp/Image/ImageLoaderPnm.hpp b/openVulkanoCpp/Image/ImageLoaderPnm.hpp new file mode 100644 index 0000000..d633506 --- /dev/null +++ b/openVulkanoCpp/Image/ImageLoaderPnm.hpp @@ -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 loadFromFile(const std::string& filePath) override; + std::unique_ptr loadFromMemory(const std::vector& buffer) override; + Math::Vector2i GetImageDimensions(const std::string& filename) override; + + private: + PnmHeader parseHeader(std::istream& stream); + Array readBinaryData(std::istream& stream, const PnmHeader& header); + Array readTextData(std::istream& stream, const PnmHeader& header); + }; +} \ No newline at end of file diff --git a/tests/Image/ImageLoaderPnmTest.cpp b/tests/Image/ImageLoaderPnmTest.cpp new file mode 100644 index 0000000..c4f008e --- /dev/null +++ b/tests/Image/ImageLoaderPnmTest.cpp @@ -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 + +#include "Image/ImageLoaderPnm.hpp" +#include "Scene/DataFormat.hpp" + +#include + +using namespace OpenVulkano; +using namespace OpenVulkano::Image; + +TEST_CASE("ImageLoaderPnm - Load P1 (ASCII Black & White)") +{ + std::vector 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 { 255, 0, 255, 0, 0, 255, 0, 255 }); +} + +TEST_CASE("ImageLoaderPnm - Load P2 (ASCII Grayscale)") +{ + std::vector 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 { 0, 64, 128, 255, 255, 128, 64, 0 }); +} + +TEST_CASE("ImageLoaderPnm - Load P3 (ASCII RGB)") +{ + std::vector 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 { 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255 }); +} + +TEST_CASE("ImageLoaderPnm - Load P4 (Binary Black & White)") +{ + std::vector 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 { + 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 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 { 0, 64, 128, 255, 255, 128, 64, 0 }); +} + +TEST_CASE("ImageLoaderPnm - Load P6 (Binary RGB)") +{ + std::vector 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 { 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()); +} \ No newline at end of file