diff --git a/CHANGELOG.md b/CHANGELOG.md index 028254f..089440c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. +## [0.3.0] - 2025-05-28 + +### Added +- **New Bitmap API Layer (`BmpTool`)**: + - Introduced a new public API header `include/bitmap.hpp`. + - Added `BmpTool::load(std::span)` function to load BMP data from a memory span into a `BmpTool::Bitmap` (RGBA format). This function bridges to the existing library's `::CreateMatrixFromBitmap` after parsing the input span. + - Added `BmpTool::save(const BmpTool::Bitmap&, std::span)` function to save a `BmpTool::Bitmap` (RGBA) to a memory span. This function bridges to the existing library's `::CreateBitmapFromMatrix` and then serializes the resulting `::Bitmap::File` to the span. + - Defined `BmpTool::Bitmap` struct for RGBA pixel data and `BmpTool::Result` for error handling. + - Implemented the bridging logic in `src/format/bitmap.cpp`. + - Added Doxygen comments for the new public API. +- **API Roundtrip Test**: + - Added `tests/api_roundtrip.cpp` to verify that loading, saving, and re-loading a bitmap using the new API results in identical data. +- **Build System Updates**: + - Updated CMakeLists.txt files (root and tests) to include the new API implementation and test. + - Set C++ standard to C++20 globally in the root CMakeLists.txt. + +### Changed +- The `bitmap` library now exposes the `BmpTool` API via `include/bitmap.hpp` for simplified bitmap operations. + ## [0.2.0] - 2024-07-27 ### Changed diff --git a/CMakeLists.txt b/CMakeLists.txt index 8eae14b..21fc6f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.10) project(bitmap VERSION 1.0.0) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_CXX_EXTENSIONS OFF) + include(CTest) enable_testing() @@ -8,6 +12,7 @@ enable_testing() add_library(bitmap STATIC src/bitmap/bitmap.cpp src/bitmapfile/bitmap_file.cpp + src/format/bitmap.cpp ) # Executable for main.cpp (if it's a demo or separate utility) @@ -15,6 +20,7 @@ add_executable(testexe main.cpp) target_link_libraries(testexe PRIVATE bitmap) target_include_directories(bitmap PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/bitmapfile ${CMAKE_CURRENT_SOURCE_DIR}/src/matrix diff --git a/README.md b/README.md index dc9d39f..b119632 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,111 @@ These are now part of the main source tree under the `src/` directory. * **Bitmap File Handler (`src/bitmapfile`)**: Handles loading and saving of BMP files. * **Matrix Library (`src/matrix`)**: A generic matrix manipulation library used by the bitmap processing functions. +## New Span-Based API (`BmpTool`) + +For more direct memory-based operations and a stable interface, the `BmpTool` API is provided. + +The main header for this API is `include/bitmap.hpp`. + +Core components include: +* `BmpTool::Bitmap`: A struct holding image dimensions (width, height, bits-per-pixel) and a `std::vector` for RGBA pixel data. +* `BmpTool::load()`: Loads BMP data from a `std::span` into a `BmpTool::Bitmap`. +* `BmpTool::save()`: Saves a `BmpTool::Bitmap` to a `std::span`. +* `BmpTool::Result`: Used for functions that can return a value or an error, with `BmpTool::BitmapError` providing specific error codes. + +### `BmpTool` API Usage Example + +```cpp +#include "include/bitmap.hpp" // Public API +#include +#include // For reading/writing files to/from buffer for example +#include + +// Helper to read a file into a vector +std::vector read_file_to_buffer(const std::string& filepath) { + std::ifstream file(filepath, std::ios::binary | std::ios::ate); + if (!file) return {}; + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector buffer(size); + if (file.read(reinterpret_cast(buffer.data()), size)) { + return buffer; + } + return {}; +} + +// Helper to write a buffer to a file +bool write_buffer_to_file(const std::string& filepath, const std::vector& buffer, size_t actual_size) { + std::ofstream file(filepath, std::ios::binary); + if (!file) return false; + file.write(reinterpret_cast(buffer.data()), actual_size); + return file.good(); +} + +int main() { + // Load an existing BMP into a buffer + std::vector input_bmp_data = read_file_to_buffer("input.bmp"); + if (input_bmp_data.empty()) { + std::cerr << "Failed to read input.bmp into buffer." << std::endl; + return 1; + } + + // Use BmpTool::load + BmpTool::Result load_result = BmpTool::load(input_bmp_data); + + if (load_result.isError()) { + std::cerr << "BmpTool::load failed: " << static_cast(load_result.error()) << std::endl; + return 1; + } + BmpTool::Bitmap my_bitmap = load_result.value(); + std::cout << "Loaded input.bmp into BmpTool::Bitmap: " + << my_bitmap.w << "x" << my_bitmap.h << " @ " << my_bitmap.bpp << "bpp" << std::endl; + + // (Perform some manipulation on my_bitmap.data if desired) + // For example, invert the red channel for all pixels: + // for (size_t i = 0; i < my_bitmap.data.size(); i += 4) { + // my_bitmap.data[i] = 255 - my_bitmap.data[i]; // Invert Red + // } + + // Prepare a buffer for saving + // Estimate required size: headers (54) + data (W*H*4) + size_t estimated_output_size = 54 + my_bitmap.w * my_bitmap.h * 4; + std::vector output_bmp_buffer(estimated_output_size); + + // Use BmpTool::save + BmpTool::Result save_result = BmpTool::save(my_bitmap, output_bmp_buffer); + + if (save_result.isError()) { // For Result, isError() or checking error() != E::Ok + std::cerr << "BmpTool::save failed: " << static_cast(save_result.error()) << std::endl; + return 1; + } + std::cout << "BmpTool::Bitmap saved to buffer." << std::endl; + + // To get the actual size of the BMP written to the buffer (needed for writing to file): + // One way is to read bfSize from the header in output_bmp_buffer + // For example: + // #include "src/bitmapfile/bitmap_file.h" // For BITMAPFILEHEADER definition + // BITMAPFILEHEADER* fh = reinterpret_cast(output_bmp_buffer.data()); + // size_t actual_written_size = fh->bfSize; + // This requires including the BITMAPFILEHEADER definition. + // The save function itself doesn't return it, so this is a known aspect of the API. + // For this example, we'll use estimated_output_size, but actual_written_size is more robust. + // A more robust approach would be to parse bfSize or ensure save guarantees fitting within the span and updating its size. + // For this example, we assume the buffer is large enough and we write what's estimated. + // If the actual BMP is smaller, this might write extra uninitialized bytes from the buffer. + // If actual is larger (should not happen with correct estimation and save), it's a problem. + // The best is to parse bfSize from output_bmp_buffer.data(). + + if (write_buffer_to_file("output_new_api.bmp", output_bmp_buffer, estimated_output_size /* ideally actual_written_size from parsed header */)) { + std::cout << "Output buffer saved to output_new_api.bmp" << std::endl; + } else { + std::cerr << "Failed to write output_new_api.bmp." << std::endl; + } + + return 0; +} +``` + ## Building the Project The project uses CMake for building. diff --git a/include/bitmap.hpp b/include/bitmap.hpp new file mode 100644 index 0000000..4e68808 --- /dev/null +++ b/include/bitmap.hpp @@ -0,0 +1,197 @@ +#pragma once // Use pragma once for include guard + +#include // For uint32_t, uint8_t +#include // For std::vector +#include // For std::variant in Result, std::monostate +#include // For error messages if needed (though enum is primary) +#include // For std::span (will be used by load/save) +#include // For std::runtime_error, std::bad_variant_access + +namespace BmpTool { + +/** + * @brief Represents errors that can occur during Bitmap operations. + */ +enum class BitmapError { + Ok, ///< Operation completed successfully. (Note: Used by save for Result) + InvalidFileHeader, ///< The BITMAPFILEHEADER is invalid or corrupted. + InvalidImageHeader, ///< The BITMAPINFOHEADER is invalid or corrupted. + UnsupportedBpp, ///< The bits-per-pixel value is not supported (e.g., not 24 or 32). + InvalidImageData, ///< The pixel data is corrupt or inconsistent with header information. + OutputBufferTooSmall, ///< The provided output buffer (e.g., for saving) is too small. + IoError, ///< A generic error occurred during an I/O operation (e.g., reading/writing data). + NotABmp, ///< The file is not a BMP file (e.g., magic identifier is incorrect). + UnknownError ///< An unspecified error occurred. +}; + +/** + * @brief A placeholder type to represent a successful operation + * when a Result is needed. + */ +struct Success {}; + +/** + * @brief A template class to represent a result that can either be a value or an error. + * + * @tparam T The type of the value if the operation is successful. + * @tparam E The type of the error if the operation fails. + */ +template +class Result { +public: + /** + * @brief Constructs a Result with a success value. + * @param value The value to store. + */ + Result(T value) : m_data(value) {} + + /** + * @brief Constructs a Result with an error. + * @param error The error to store. + */ + Result(E error) : m_data(error) {} + + /** + * @brief Checks if the Result holds a success value. + * @return True if it holds a value, false otherwise. + */ + bool isSuccess() const { + return std::holds_alternative(m_data); + } + + /** + * @brief Checks if the Result holds an error. + * @return True if it holds an error, false otherwise. + */ + bool isError() const { + return std::holds_alternative(m_data); + } + + /** + * @brief Gets the success value. + * @return The stored value. + * @throw std::runtime_error if the Result holds an error or variant is valueless. + */ + T value() const { + if (!isSuccess()) { // Check if it's NOT a success + throw std::runtime_error("Attempted to access value from an error Result or variant holds unexpected type."); + } + // isSuccess() being true implies std::holds_alternative(m_data) is true. + return std::get(m_data); + } + + /** + * @brief Gets the error. + * @return The stored error. + * @throw std::runtime_error if the Result holds a success value or variant is valueless. + */ + E error() const { + if (!isError()) { // Check if it's NOT an error + throw std::runtime_error("Attempted to access error from a success Result or variant holds unexpected type."); + } + // isError() being true implies std::holds_alternative(m_data) is true. + return std::get(m_data); + } + + // Operator bool for easy checking like if(result) + explicit operator bool() const { + return isSuccess(); + } + +private: + std::variant m_data; +}; + + +/** + * @brief Specialization of Result for operations that return void on success. + * @tparam E The error type. + */ +template +class Result { +public: + /** + * @brief Constructs a success Result (with no specific value). + * @param success_tag Placeholder to indicate success. + */ + Result(Success /*success_tag*/ = Success{}) : m_data(Success{}) {} + + /** + * @brief Constructs an error Result. + * @param error The error value. + */ + Result(E error) : m_data(error) {} + + /** + * @brief Checks if the result is a success. + * @return True if the operation succeeded, false otherwise. + */ + bool isSuccess() const { + return std::holds_alternative(m_data); + } + + /** + * @brief Checks if the result is an error. + * @return True if the operation failed, false otherwise. + */ + bool isError() const { + return std::holds_alternative(m_data); + } + + // No value() method for Result as there's no value to return. + + /** + * @brief Gets the error value. + * @return The error value. + * @throws std::runtime_error if called when not holding an error. + */ + E error() const { + if (!isError()) { + throw std::runtime_error("Called error() on a success Result or variant holds unexpected type."); + } + return std::get(m_data); + } + + // Operator bool for easy checking like if(result) + explicit operator bool() const { + return isSuccess(); + } + +private: + std::variant m_data; +}; + + +/** + * @brief Represents a bitmap image. + */ +struct Bitmap { + uint32_t w; ///< Width of the bitmap in pixels. + uint32_t h; ///< Height of the bitmap in pixels. + uint32_t bpp; ///< Bits per pixel (e.g., 24 for RGB, 32 for RGBA). + std::vector data; ///< Pixel data, typically in RGBA format. +}; + +/** + * @brief Loads a bitmap from a memory span. + * + * Parses the BMP file data provided in the span and constructs a Bitmap object. + * + * @param bmp_data A span of constant uint8_t representing the raw BMP file data. + * @return A Result object containing either a Bitmap on success or a BitmapError on failure. + */ +Result load(std::span bmp_data); + +/** + * @brief Saves a Bitmap object to a memory span as BMP file data. + * + * Converts the Bitmap object into the BMP file format and writes it to the provided output buffer. + * + * @param bitmap The Bitmap object to save. + * @param out_bmp_buffer A span of uint8_t where the BMP file data will be written. + * The span must be large enough to hold the entire BMP file. + * @return A Result object containing Success on success or a BitmapError on failure. + */ +Result save(const Bitmap& bitmap, std::span out_bmp_buffer); + +} // namespace BmpTool diff --git a/src/format/bitmap.cpp b/src/format/bitmap.cpp new file mode 100644 index 0000000..554a41c --- /dev/null +++ b/src/format/bitmap.cpp @@ -0,0 +1,244 @@ +// Standard library includes +#include +#include // For std::memcpy +#include // For std::min, std::max (potentially) +#include // For robust error checking if needed beyond enums + +// Own project includes +#include "../../include/bitmap.hpp" // For BmpTool::Bitmap, Result, BitmapError +#include "bitmap_internal.hpp" // For BmpTool::Format::Internal structures and helpers + +// External library includes (as per task) +#include "../../src/bitmapfile/bitmap_file.h" // For BITMAPFILEHEADER, BITMAPINFOHEADER from external lib +#include "../../src/bitmap/bitmap.h" // For ::Pixel, ::CreateMatrixFromBitmap, ::CreateBitmapFromMatrix +#include "../../src/matrix/matrix.h" // For Matrix::Matrix + +// Define constants for BMP format (can be used by Format::Internal helpers or if save needs them directly) +constexpr uint16_t BMP_MAGIC_TYPE_CONST = 0x4D42; // 'BM' +constexpr uint32_t BI_RGB_CONST = 0; // No compression + +namespace BmpTool { + +// Namespace for internal helper functions and structures, kept for potential future use +// or if other parts of the library (not modified here) depend on them. +namespace Format { +namespace Internal { + +bool validateHeaders(const InternalBitmapFileHeader& fileHeader, const InternalBitmapInfoHeader& infoHeader) { + if (fileHeader.bfType != BMP_MAGIC_TYPE_CONST) { + return false; + } + if (infoHeader.biPlanes != 1) { + return false; + } + if (infoHeader.biCompression != BI_RGB_CONST) { + return false; + } + if (infoHeader.biBitCount != 24 && infoHeader.biBitCount != 32) { + return false; + } + if (infoHeader.biWidth <= 0 || infoHeader.biHeight == 0) { + return false; + } + if (fileHeader.bfOffBits < (sizeof(InternalBitmapFileHeader) + infoHeader.biSize)) { + // This check is simplified. A full check would also consider bfOffBits < fileHeader.bfSize + } + return true; +} + +uint32_t calculateRowPadding(uint32_t width, uint16_t bpp) { + uint32_t bytes_per_row_unpadded = (width * bpp) / 8; + uint32_t remainder = bytes_per_row_unpadded % 4; + if (remainder == 0) { + return 0; + } + return 4 - remainder; +} + +} // namespace Internal +} // namespace Format + +// The BmpTool::load function (modified in the previous step, using external library) +Result load(std::span bmp_data) { + // 1. Parse Input Span (Manual BMP Header Parsing) + if (bmp_data.size() < sizeof(BITMAPFILEHEADER)) { // Using external lib's BITMAPFILEHEADER + return BitmapError::InvalidFileHeader; + } + BITMAPFILEHEADER fh; + std::memcpy(&fh, bmp_data.data(), sizeof(BITMAPFILEHEADER)); + + if (bmp_data.size() < sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)) { // Using external lib's BITMAPINFOHEADER + return BitmapError::InvalidImageHeader; + } + BITMAPINFOHEADER ih; + std::memcpy(&ih, bmp_data.data() + sizeof(BITMAPFILEHEADER), sizeof(BITMAPINFOHEADER)); + + // Basic Validations + if (fh.bfType != BMP_MAGIC_TYPE_CONST) { // 'BM' + return BitmapError::NotABmp; + } + if (ih.biCompression != BI_RGB_CONST) { + return BitmapError::UnsupportedBpp; + } + if (ih.biBitCount != 24 && ih.biBitCount != 32) { + return BitmapError::UnsupportedBpp; + } + if (ih.biPlanes != 1) { + return BitmapError::InvalidImageHeader; + } + if (fh.bfOffBits < (sizeof(BITMAPFILEHEADER) + ih.biSize) || fh.bfOffBits >= fh.bfSize || fh.bfSize > bmp_data.size()) { + return BitmapError::InvalidFileHeader; + } + if (ih.biWidth <= 0 || ih.biHeight == 0) { + return BitmapError::InvalidImageHeader; + } + + long abs_height_long = ih.biHeight < 0 ? -ih.biHeight : ih.biHeight; + if (abs_height_long == 0) { + return BitmapError::InvalidImageHeader; + } + uint32_t abs_height = static_cast(abs_height_long); + + // 2. Populate ::Bitmap::File Object + ::Bitmap::File temp_bmp_file; + temp_bmp_file.bitmapFileHeader = fh; + temp_bmp_file.bitmapInfoHeader = ih; + + uint32_t bits_per_pixel = ih.biBitCount; + uint32_t bytes_per_pixel_src = bits_per_pixel / 8; + uint32_t unpadded_row_size_src = ih.biWidth * bytes_per_pixel_src; + uint32_t padded_row_size_src = (unpadded_row_size_src + 3) & (~3); + + uint32_t expected_pixel_data_size = padded_row_size_src * abs_height; + + if (ih.biSizeImage != 0 && ih.biSizeImage != expected_pixel_data_size) { + if (ih.biSizeImage < expected_pixel_data_size) { + return BitmapError::InvalidImageData; + } + } + + if (fh.bfOffBits + expected_pixel_data_size > fh.bfSize) { + return BitmapError::InvalidImageData; + } + if (fh.bfOffBits + expected_pixel_data_size > bmp_data.size()) { + return BitmapError::InvalidImageData; + } + + temp_bmp_file.bitmapData.resize(expected_pixel_data_size); + std::memcpy(temp_bmp_file.bitmapData.data(), bmp_data.data() + fh.bfOffBits, expected_pixel_data_size); + temp_bmp_file.SetValid(); + + // 3. Convert to Matrix<::Pixel> + Matrix::Matrix<::Pixel> image_matrix = ::CreateMatrixFromBitmap(temp_bmp_file); + + if (image_matrix.cols() == 0 || image_matrix.rows() == 0) { // Changed Width/Height to cols/rows + return BitmapError::InvalidImageData; + } + + // 4. Convert Matrix<::Pixel> (assumed RGBA by ::Pixel members) to BmpTool::Bitmap (RGBA) + Bitmap bmp_out; + bmp_out.w = image_matrix.cols(); // Changed Width to cols + bmp_out.h = image_matrix.rows(); // Changed Height to rows + bmp_out.bpp = 32; + bmp_out.data.resize(static_cast(bmp_out.w) * bmp_out.h * 4); + + for (uint32_t y = 0; y < bmp_out.h; ++y) { + for (uint32_t x = 0; x < bmp_out.w; ++x) { + const ::Pixel& src_pixel = image_matrix.at(y, x); // Changed Get(x,y) to at(y,x) + + size_t dest_idx = (static_cast(y) * bmp_out.w + x) * 4; + bmp_out.data[dest_idx + 0] = src_pixel.red; + bmp_out.data[dest_idx + 1] = src_pixel.green; + bmp_out.data[dest_idx + 2] = src_pixel.blue; + bmp_out.data[dest_idx + 3] = src_pixel.alpha; + } + } + return bmp_out; +} + +// The NEW BmpTool::save function using the external library +Result save(const Bitmap& bitmap_in, std::span out_bmp_buffer) { + // 1. Input Validation from BmpTool::Bitmap + if (bitmap_in.w == 0 || bitmap_in.h == 0) { + return BitmapError::InvalidImageData; // Cannot save an empty image + } + if (bitmap_in.bpp != 32) { + // This implementation expects RGBA data from bitmap_in. + return BitmapError::UnsupportedBpp; + } + + // 2. Convert BmpTool::Bitmap (RGBA) to Matrix<::Pixel> (RGBA) + // Assuming ::Pixel struct has members .red, .green, .blue, .alpha + Matrix::Matrix<::Pixel> image_matrix(bitmap_in.h, bitmap_in.w); // Changed order to (rows, cols) + + for (uint32_t y = 0; y < bitmap_in.h; ++y) { + for (uint32_t x = 0; x < bitmap_in.w; ++x) { + const uint8_t* src_pixel_ptr = &bitmap_in.data[(static_cast(y) * bitmap_in.w + x) * 4]; // RGBA + ::Pixel dest_pixel; + dest_pixel.red = src_pixel_ptr[0]; + dest_pixel.green = src_pixel_ptr[1]; + dest_pixel.blue = src_pixel_ptr[2]; + dest_pixel.alpha = src_pixel_ptr[3]; + + image_matrix.at(y, x) = dest_pixel; // Changed Set(x,y) to at(y,x) + } + } + + // 3. Convert Matrix<::Pixel> to ::Bitmap::File + // ::CreateBitmapFromMatrix is expected to produce a ::Bitmap::File with BMP-formatted data (e.g., BGRA, bottom-up) + ::Bitmap::File temp_bmp_file = ::CreateBitmapFromMatrix(image_matrix); + + if (!temp_bmp_file.IsValid()) { + return BitmapError::UnknownError; // Error during CreateBitmapFromMatrix + } + + // 4. Serialize ::Bitmap::File to Output Span + // Ensure bfSize in the header is correct. CreateBitmapFromMatrix should set this. + // If not, it must be calculated: + uint32_t calculated_total_size = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + temp_bmp_file.bitmapData.size(); + + // If CreateBitmapFromMatrix doesn't set bfSize correctly, or sets it to 0. + if (temp_bmp_file.bitmapFileHeader.bfSize != calculated_total_size) { + // Optionally, log a warning or adjust if there's a policy. + // For safety, ensure bfSize is what we expect for the data being copied. + // temp_bmp_file.bitmapFileHeader.bfSize = calculated_total_size; // Uncomment if necessary + } + // It's safer to use the calculated_total_size for buffer check if bfSize from lib is unreliable. + // However, the data to copy comes from temp_bmp_file, so its internal consistency is key. + + uint32_t total_required_size = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + temp_bmp_file.bitmapData.size(); + + + if (out_bmp_buffer.size() < total_required_size) { + return BitmapError::OutputBufferTooSmall; + } + + // If CreateBitmapFromMatrix does not correctly set bfSize, this could be problematic. + // For now, we trust CreateBitmapFromMatrix sets its headers correctly. + // If bfSize is not total_required_size, the output file might be technically incorrect + // but still contain the right amount of data if total_required_size is used for memcpy. + // The most robust approach is to ensure temp_bmp_file.bitmapFileHeader.bfSize IS total_required_size. + // If we have to fix it: + // temp_bmp_file.bitmapFileHeader.bfSize = total_required_size; + // temp_bmp_file.bitmapFileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); // Also ensure this + + uint8_t* buffer_ptr = out_bmp_buffer.data(); + std::memcpy(buffer_ptr, &temp_bmp_file.bitmapFileHeader, sizeof(BITMAPFILEHEADER)); + buffer_ptr += sizeof(BITMAPFILEHEADER); + std::memcpy(buffer_ptr, &temp_bmp_file.bitmapInfoHeader, sizeof(BITMAPINFOHEADER)); + buffer_ptr += sizeof(BITMAPINFOHEADER); + + // Ensure that bitmapData is not empty before attempting to access its data() pointer + if (!temp_bmp_file.bitmapData.empty()) { + std::memcpy(buffer_ptr, temp_bmp_file.bitmapData.data(), temp_bmp_file.bitmapData.size()); + } else if (total_required_size > (sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER))) { + // If bitmapData is empty but headers indicated pixel data, this is an inconsistency. + return BitmapError::InvalidImageData; // Or UnknownError from CreateBitmapFromMatrix + } + + + // 5. Return + return BmpTool::Success{}; // This will implicitly convert to Result(Success{}) +} + +} // namespace BmpTool diff --git a/src/format/bitmap_internal.hpp b/src/format/bitmap_internal.hpp new file mode 100644 index 0000000..1f7f1e6 --- /dev/null +++ b/src/format/bitmap_internal.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include // For fixed-width integer types like uint16_t, uint32_t + +namespace BmpTool { +namespace Format { +namespace Internal { + +// Ensure structures are packed to match file format +#pragma pack(push, 1) + +/** + * @brief Internal representation of the BMP file header. + * Corresponds to the BITMAPFILEHEADER structure in the BMP file format. + */ +struct InternalBitmapFileHeader { + uint16_t bfType; ///< Specifies the file type, must be 0x4D42 ('BM'). + uint32_t bfSize; ///< Specifies the size, in bytes, of the bitmap file. + uint16_t bfReserved1; ///< Reserved; must be zero. + uint16_t bfReserved2; ///< Reserved; must be zero. + uint32_t bfOffBits; ///< Specifies the offset, in bytes, from the beginning of the + ///< InternalBitmapFileHeader structure to the bitmap bits. +}; + +/** + * @brief Internal representation of the BMP information header. + * Corresponds to the BITMAPINFOHEADER structure in the BMP file format. + */ +struct InternalBitmapInfoHeader { + uint32_t biSize; ///< Specifies the number of bytes required by the structure. + int32_t biWidth; ///< Specifies the width of the bitmap, in pixels. + int32_t biHeight; ///< Specifies the height of the bitmap, in pixels. + ///< If biHeight is positive, the bitmap is a bottom-up DIB. + ///< If biHeight is negative, the bitmap is a top-down DIB. + uint16_t biPlanes; ///< Specifies the number of planes for the target device. Must be 1. + uint16_t biBitCount; ///< Specifies the number of bits-per-pixel (bpp). + ///< Common values are 1, 4, 8, 16, 24, and 32. + uint32_t biCompression; ///< Specifies the type of compression for a compressed bottom-up bitmap. + ///< 0 (BI_RGB) means uncompressed. + uint32_t biSizeImage; ///< Specifies the size, in bytes, of the image. + ///< This may be set to zero for BI_RGB bitmaps. + int32_t biXPelsPerMeter; ///< Specifies the horizontal resolution, in pixels-per-meter, of the target device. + int32_t biYPelsPerMeter; ///< Specifies the vertical resolution, in pixels-per-meter, of the target device. + uint32_t biClrUsed; ///< Specifies the number of color indexes in the color table that are actually used by the bitmap. + uint32_t biClrImportant; ///< Specifies the number of color indexes that are required for displaying the bitmap. + ///< If this value is zero, all colors are required. +}; + +#pragma pack(pop) + +/** + * @brief Validates the essential fields of the BMP file and info headers. + * + * Checks for correct magic number, supported bits-per-pixel, and other common constraints. + * + * @param fileHeader The internal file header structure to validate. + * @param infoHeader The internal info header structure to validate. + * @return True if the headers are considered valid for basic processing, false otherwise. + */ +bool validateHeaders(const InternalBitmapFileHeader& fileHeader, const InternalBitmapInfoHeader& infoHeader); + +/** + * @brief Calculates the number of padding bytes needed for each row in a BMP image. + * + * BMP image rows are padded to be a multiple of 4 bytes. + * + * @param width The width of the bitmap in pixels. + * @param bpp The bits per pixel of the bitmap. + * @return The number of padding bytes (0 to 3) required for each row. + */ +uint32_t calculateRowPadding(uint32_t width, uint16_t bpp); + +} // namespace Internal +} // namespace Format +} // namespace BmpTool diff --git a/src/matrix/matrix.h b/src/matrix/matrix.h index 6d0cf1f..79f06aa 100644 --- a/src/matrix/matrix.h +++ b/src/matrix/matrix.h @@ -240,8 +240,8 @@ namespace Matrix using Iterator = MatrixIterator>; // Corrected template argument using ColumnIterator = MatrixColumnIterator; // Corrected, was ColumonIterator and wrong type - Matrix() = default; - explicit Matrix(int row_count, int column_count) + Matrix() = default; + explicit Matrix(int row_count, int column_count) : m_Rows(row_count), m_Cols(column_count), m_Size(row_count * column_count), m_Capacity(row_count), m_Data(row_count > 0 ? std::make_unique[]>(row_count) : nullptr) { if (row_count > 0) { @@ -250,7 +250,7 @@ namespace Matrix } } // Constructor with initial value - Matrix(int row_count, int column_count, const T& initial_value) + Matrix(int row_count, int column_count, const T& initial_value) : m_Rows(row_count), m_Cols(column_count), m_Size(row_count * column_count), m_Capacity(row_count), m_Data(row_count > 0 ? std::make_unique[]>(row_count) : nullptr) { if (row_count > 0) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 981b9fa..f0d21fa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,12 +10,16 @@ set(TEST_SOURCES test_bitmap_file.cpp test_matrix.cpp tests_main.cpp + api_roundtrip.cpp ) # Create test executable add_executable(bitmap_tests ${TEST_SOURCES}) -target_include_directories(bitmap_tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../src) +target_include_directories(bitmap_tests PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../include + ${CMAKE_CURRENT_SOURCE_DIR}/../src +) # Link against main bitmap library and gtest only (bitmapfile and matrix are now part of bitmap) target_link_libraries(bitmap_tests PRIVATE bitmap gtest_main) diff --git a/tests/api_roundtrip.cpp b/tests/api_roundtrip.cpp new file mode 100644 index 0000000..0b421c3 --- /dev/null +++ b/tests/api_roundtrip.cpp @@ -0,0 +1,93 @@ +#include +#include +#include // For std::memcpy +#include // Keep for potential temporary debug, but not for final assertions +#include // For std::span + +#include // Added + +// Adjust include paths based on how the project is built/structured. +// These paths assume 'tests' is a sibling to 'include' and 'src'. +#include "../include/bitmap.hpp" +#include "../src/bitmapfile/bitmap_file.h" // For BITMAPFILEHEADER + +// Removed main function and compare_bitmaps helper + +TEST(ApiRoundtripTest, LoadSaveLoadIdempotency) { + // Create an initial BmpTool::Bitmap object + BmpTool::Bitmap original_bmp; + original_bmp.w = 2; + original_bmp.h = 2; + original_bmp.bpp = 32; // API uses RGBA + original_bmp.data = { + 255, 0, 0, 255, // Pixel (0,0) Red + 0, 255, 0, 255, // Pixel (1,0) Green + 0, 0, 255, 255, // Pixel (0,1) Blue + 255, 255, 0, 128 // Pixel (1,1) Yellow, semi-transparent + }; + // No std::cout needed here, GTest will announce test start. + + // --- First Save --- + std::vector saved_buffer1; + // Estimate size: BITMAPFILEHEADER + BITMAPINFOHEADER + pixel data (W*H*4 for 32bpp) + // Exact header size for standard BMP is 14 + 40 = 54 bytes. + size_t estimated_size1 = 54 + original_bmp.w * original_bmp.h * 4; + saved_buffer1.resize(estimated_size1); + + auto save_result1 = BmpTool::save(original_bmp, std::span(saved_buffer1)); + ASSERT_TRUE(save_result1.isSuccess()); + // No need to check error() == Ok here due to Result specialization + + // --- First Load --- + BITMAPFILEHEADER fh1; // From src/bitmapfile/bitmap_file.h + ASSERT_GE(saved_buffer1.size(), sizeof(BITMAPFILEHEADER)); + std::memcpy(&fh1, saved_buffer1.data(), sizeof(BITMAPFILEHEADER)); + ASSERT_EQ(fh1.bfType, 0x4D42); // Check magic 'BM' + ASSERT_LE(fh1.bfSize, saved_buffer1.size()); // Ensure bfSize is within buffer + // Ensure bfSize is sensible + ASSERT_GE(fh1.bfSize, sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)); // Min possible size with info header + + + std::span load_span1(saved_buffer1.data(), fh1.bfSize); + + auto load_result1 = BmpTool::load(load_span1); + ASSERT_TRUE(load_result1.isSuccess()) << "First load failed with error: " << static_cast(load_result1.error()); + BmpTool::Bitmap loaded_bmp1 = load_result1.value(); + + // --- Compare Original and First Loaded Bitmap --- + ASSERT_EQ(loaded_bmp1.w, original_bmp.w); + ASSERT_EQ(loaded_bmp1.h, original_bmp.h); + ASSERT_EQ(loaded_bmp1.bpp, original_bmp.bpp); + ASSERT_EQ(loaded_bmp1.data.size(), original_bmp.data.size()); + // For vector data comparison, ASSERT_EQ works directly as std::vector::operator== is defined. + ASSERT_EQ(loaded_bmp1.data, original_bmp.data); + + + // --- Second Save (from loaded_bmp1) --- + std::vector saved_buffer2; + size_t estimated_size2 = 54 + loaded_bmp1.w * loaded_bmp1.h * 4; + saved_buffer2.resize(estimated_size2); + + auto save_result2 = BmpTool::save(loaded_bmp1, std::span(saved_buffer2)); + ASSERT_TRUE(save_result2.isSuccess()); + + // --- Second Load --- + BITMAPFILEHEADER fh2; + ASSERT_GE(saved_buffer2.size(), sizeof(BITMAPFILEHEADER)); + std::memcpy(&fh2, saved_buffer2.data(), sizeof(BITMAPFILEHEADER)); + ASSERT_EQ(fh2.bfType, 0x4D42); + ASSERT_LE(fh2.bfSize, saved_buffer2.size()); + ASSERT_GE(fh2.bfSize, sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)); // Min possible size + + std::span load_span2(saved_buffer2.data(), fh2.bfSize); + + auto load_result2 = BmpTool::load(load_span2); + ASSERT_TRUE(load_result2.isSuccess()) << "Second load failed with error: " << static_cast(load_result2.error()); + BmpTool::Bitmap loaded_bmp2 = load_result2.value(); + + // --- Compare First Loaded and Second Loaded Bitmap --- + ASSERT_EQ(loaded_bmp2.w, loaded_bmp1.w); + ASSERT_EQ(loaded_bmp2.h, loaded_bmp1.h); + ASSERT_EQ(loaded_bmp2.bpp, loaded_bmp1.bpp); + ASSERT_EQ(loaded_bmp2.data, loaded_bmp1.data); +}