#include #include "converter/output/OutputWriter.h" #include "converter/pipeline/ImageData.h" #include #include #include using namespace photoconv; namespace fs = std::filesystem; /** * @brief Create a simple test image with known dimensions. */ static ImageData make_test_image(int width, int height) { ImageData data; data.rgb = cv::Mat(height, width, CV_16UC3, cv::Scalar(32768, 32768, 32768)); data.source_path = "test_image.arw"; data.metadata.camera_make = "Test"; return data; } // ────────────────────────────────────────────── // OutputWriter tests // ────────────────────────────────────────────── TEST(OutputWriterTest, WritesValidPNG16) { const auto temp_dir = fs::temp_directory_path() / "photoconv_test_output"; fs::remove_all(temp_dir); // Clean up from previous runs fs::create_directories(temp_dir); OutputConfig config{}; config.output_dir = temp_dir; config.format = OutputFormat::PNG_16bit; OutputWriter writer{config}; auto data = make_test_image(100, 100); auto result = writer.process(std::move(data)); ASSERT_TRUE(result.has_value()) << result.error().message; // Verify file was created const auto expected_path = temp_dir / "test_image_converted.png"; EXPECT_TRUE(fs::exists(expected_path)); // Verify it's a valid PNG that can be read back cv::Mat loaded = cv::imread(expected_path.string(), cv::IMREAD_UNCHANGED); ASSERT_FALSE(loaded.empty()); EXPECT_EQ(loaded.type(), CV_16UC3); EXPECT_EQ(loaded.cols, 100); EXPECT_EQ(loaded.rows, 100); fs::remove_all(temp_dir); } TEST(OutputWriterTest, WritesValidPNG8) { const auto temp_dir = fs::temp_directory_path() / "photoconv_test_output"; fs::remove_all(temp_dir); fs::create_directories(temp_dir); OutputConfig config{}; config.output_dir = temp_dir; config.format = OutputFormat::PNG_8bit; OutputWriter writer{config}; auto data = make_test_image(100, 100); auto result = writer.process(std::move(data)); ASSERT_TRUE(result.has_value()); // Verify file was created and is 8-bit const auto expected_path = temp_dir / "test_image_converted.png"; EXPECT_TRUE(fs::exists(expected_path)); cv::Mat loaded = cv::imread(expected_path.string(), cv::IMREAD_UNCHANGED); ASSERT_FALSE(loaded.empty()); EXPECT_EQ(loaded.type(), CV_8UC3); EXPECT_EQ(loaded.cols, 100); EXPECT_EQ(loaded.rows, 100); fs::remove_all(temp_dir); } TEST(OutputWriterTest, WritesValidTIFF16) { const auto temp_dir = fs::temp_directory_path() / "photoconv_test_output"; fs::remove_all(temp_dir); fs::create_directories(temp_dir); OutputConfig config{}; config.output_dir = temp_dir; config.format = OutputFormat::TIFF_16bit; OutputWriter writer{config}; auto data = make_test_image(100, 100); auto result = writer.process(std::move(data)); ASSERT_TRUE(result.has_value()); // Verify file has .tif extension const auto expected_path = temp_dir / "test_image_converted.tif"; EXPECT_TRUE(fs::exists(expected_path)); cv::Mat loaded = cv::imread(expected_path.string(), cv::IMREAD_UNCHANGED); ASSERT_FALSE(loaded.empty()); EXPECT_EQ(loaded.type(), CV_16UC3); fs::remove_all(temp_dir); } TEST(OutputWriterTest, WritesValidJPEG) { const auto temp_dir = fs::temp_directory_path() / "photoconv_test_output"; fs::remove_all(temp_dir); fs::create_directories(temp_dir); OutputConfig config{}; config.output_dir = temp_dir; config.format = OutputFormat::JPEG; config.jpeg_quality = 85; OutputWriter writer{config}; auto data = make_test_image(100, 100); auto result = writer.process(std::move(data)); ASSERT_TRUE(result.has_value()); // Verify file has .jpg extension const auto expected_path = temp_dir / "test_image_converted.jpg"; EXPECT_TRUE(fs::exists(expected_path)); // JPEG loads as 8-bit cv::Mat loaded = cv::imread(expected_path.string(), cv::IMREAD_UNCHANGED); ASSERT_FALSE(loaded.empty()); EXPECT_EQ(loaded.type(), CV_8UC3); fs::remove_all(temp_dir); } TEST(OutputWriterTest, CreatesOutputDirectory) { const auto temp_dir = fs::temp_directory_path() / "photoconv_test_output" / "nested" / "path"; fs::remove_all(temp_dir.parent_path()); OutputConfig config{}; config.output_dir = temp_dir; config.format = OutputFormat::PNG_16bit; OutputWriter writer{config}; auto data = make_test_image(50, 50); auto result = writer.process(std::move(data)); ASSERT_TRUE(result.has_value()); EXPECT_TRUE(fs::exists(temp_dir)); fs::remove_all(temp_dir.parent_path().parent_path()); } TEST(OutputWriterTest, RejectsEmptyImage) { const auto temp_dir = fs::temp_directory_path() / "photoconv_test_output"; fs::remove_all(temp_dir); fs::create_directories(temp_dir); OutputConfig config{}; config.output_dir = temp_dir; config.format = OutputFormat::PNG_16bit; OutputWriter writer{config}; ImageData data; // Empty image auto result = writer.process(std::move(data)); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error().code, ErrorCode::OutputWriteFailed); fs::remove_all(temp_dir); } TEST(OutputWriterTest, Preserves16BitPixelValues) { const auto temp_dir = fs::temp_directory_path() / "photoconv_test_output"; fs::remove_all(temp_dir); fs::create_directories(temp_dir); OutputConfig config{}; config.output_dir = temp_dir; config.format = OutputFormat::PNG_16bit; OutputWriter writer{config}; // Create image with known pixel values ImageData data; data.rgb = cv::Mat(10, 10, CV_16UC3); for (int y = 0; y < 10; ++y) { for (int x = 0; x < 10; ++x) { data.rgb.at(y, x) = {10000, 20000, 30000}; } } data.source_path = "test_values.arw"; data.metadata.camera_make = "Test"; auto result = writer.process(std::move(data)); ASSERT_TRUE(result.has_value()); // Read back and verify pixel values are preserved const auto expected_path = temp_dir / "test_values_converted.png"; cv::Mat loaded = cv::imread(expected_path.string(), cv::IMREAD_UNCHANGED); ASSERT_FALSE(loaded.empty()); // Allow small tolerance due to PNG compression/decompression const auto pixel = loaded.at(5, 5); EXPECT_NEAR(pixel[0], 10000, 1.0); EXPECT_NEAR(pixel[1], 20000, 1.0); EXPECT_NEAR(pixel[2], 30000, 1.0); fs::remove_all(temp_dir); } TEST(OutputWriterTest, Converts16BitTo8BitForPNG8) { const auto temp_dir = fs::temp_directory_path() / "photoconv_test_output"; fs::remove_all(temp_dir); fs::create_directories(temp_dir); OutputConfig config{}; config.output_dir = temp_dir; config.format = OutputFormat::PNG_8bit; OutputWriter writer{config}; // Create 16-bit image with specific values ImageData data; data.rgb = cv::Mat(10, 10, CV_16UC3); for (int y = 0; y < 10; ++y) { for (int x = 0; x < 10; ++x) { // 16-bit value 32768 should convert to 8-bit value 128 (32768 / 257 ≈ 128) data.rgb.at(y, x) = {32768, 32768, 32768}; } } data.source_path = "test_conversion.arw"; data.metadata.camera_make = "Test"; auto result = writer.process(std::move(data)); ASSERT_TRUE(result.has_value()); // Read back as 8-bit const auto expected_path = temp_dir / "test_conversion_converted.png"; cv::Mat loaded = cv::imread(expected_path.string(), cv::IMREAD_UNCHANGED); ASSERT_FALSE(loaded.empty()); EXPECT_EQ(loaded.type(), CV_8UC3); // Check the converted value const auto pixel = loaded.at(5, 5); EXPECT_EQ(pixel[0], 128); EXPECT_EQ(pixel[1], 128); EXPECT_EQ(pixel[2], 128); fs::remove_all(temp_dir); }