chore: initial project scaffold from architecture design

- Add CLAUDE.md with project overview, tech stack, build commands,
  architecture description, coding standards, and sample images section
- Add full directory structure: src/, docs/, tests/, import/
- Add CMakeLists.txt with C++20, OpenCV/LibRaw/Qt6 dependencies,
  converter_core static lib, optional GUI, and GTest tests
- Add architecture documentation: ARCHITECTURE.md, PIPELINE.md, MODULES.md
- Add source skeletons for all pipeline stages:
  RawLoader, Preprocessor, NegativeDetector, Inverter, ColorCorrector,
  CropProcessor, OutputWriter, Pipeline, MainWindow, CliRunner, main.cpp
- Add initial test stubs for pipeline and rawloader
- Add sample ARW files in import/ for integration testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-03-14 09:28:32 +01:00
commit 65b411b23d
34 changed files with 3191 additions and 0 deletions

149
src/cli/CliRunner.cpp Normal file
View File

@@ -0,0 +1,149 @@
#include "CliRunner.h"
#include "../converter/rawloader/RawLoader.h"
#include "../converter/preprocess/Preprocessor.h"
#include "../converter/negative/NegativeDetector.h"
#include "../converter/invert/Inverter.h"
#include "../converter/color/ColorCorrector.h"
#include "../converter/crop/CropProcessor.h"
#include "../converter/output/OutputWriter.h"
#include <format>
#include <iostream>
namespace photoconv {
std::expected<CliRunner::Config, Error> CliRunner::parse_args(int argc, char* argv[]) {
Config config;
bool reading_inputs = false;
for (int i = 1; i < argc; ++i) {
std::string arg{argv[i]};
if (arg == "-i" || arg == "--input") {
reading_inputs = true;
continue;
}
if (arg == "-o" || arg == "--output") {
reading_inputs = false;
if (i + 1 < argc) {
config.output_dir = argv[++i];
} else {
return std::unexpected(make_error(
ErrorCode::InvalidArgument, "Missing value for --output"));
}
continue;
}
if (arg == "--format") {
reading_inputs = false;
if (i + 1 < argc) {
config.output_format = argv[++i];
} else {
return std::unexpected(make_error(
ErrorCode::InvalidArgument, "Missing value for --format"));
}
continue;
}
if (arg == "--quality") {
reading_inputs = false;
if (i + 1 < argc) {
config.jpeg_quality = std::stoi(argv[++i]);
}
continue;
}
if (arg == "-v" || arg == "--verbose") {
config.verbose = true;
reading_inputs = false;
continue;
}
if (arg == "--cli") {
reading_inputs = false;
continue;
}
if (arg == "-h" || arg == "--help") {
std::cout <<
"Usage: photo-converter --cli -i <files...> -o <output_dir> [options]\n"
"\n"
"Options:\n"
" -i, --input <files...> Input image files (RAW or standard)\n"
" -o, --output <dir> Output directory (default: output/)\n"
" --format <fmt> Output format: png16, png8, tiff16, jpeg\n"
" --quality <0-100> JPEG quality (default: 95)\n"
" -v, --verbose Verbose output\n"
" -h, --help Show this help\n"
<< std::endl;
return std::unexpected(make_error(ErrorCode::InvalidArgument, "Help requested"));
}
// If reading inputs or no flag active, treat as input file
if (reading_inputs || arg[0] != '-') {
config.input_files.emplace_back(arg);
reading_inputs = true;
}
}
if (config.input_files.empty()) {
return std::unexpected(make_error(
ErrorCode::InvalidArgument, "No input files specified. Use -i <files...>"));
}
return config;
}
std::expected<int, Error> CliRunner::run(const Config& config) const {
// Determine output format
OutputFormat fmt = OutputFormat::PNG_16bit;
if (config.output_format == "png8") fmt = OutputFormat::PNG_8bit;
else if (config.output_format == "tiff16") fmt = OutputFormat::TIFF_16bit;
else if (config.output_format == "jpeg") fmt = OutputFormat::JPEG;
// Build pipeline
Pipeline pipeline;
pipeline.add_stage(std::make_unique<Preprocessor>());
pipeline.add_stage(std::make_unique<NegativeDetector>());
pipeline.add_stage(std::make_unique<Inverter>());
pipeline.add_stage(std::make_unique<ColorCorrector>());
pipeline.add_stage(std::make_unique<CropProcessor>());
pipeline.add_stage(std::make_unique<OutputWriter>(
OutputConfig{config.output_dir, fmt, config.jpeg_quality}));
RawLoader loader;
int success_count = 0;
for (const auto& file : config.input_files) {
std::cout << std::format("\n[CLI] Processing: {}", file.string()) << std::endl;
auto load_result = loader.load(file);
if (!load_result.has_value()) {
std::cerr << std::format("[CLI] Load failed: {}",
load_result.error().format()) << std::endl;
continue;
}
auto result = pipeline.execute(std::move(load_result.value()));
if (!result.has_value()) {
std::cerr << std::format("[CLI] Pipeline failed: {}",
result.error().format()) << std::endl;
continue;
}
++success_count;
}
std::cout << std::format("\n[CLI] Done: {}/{} files processed successfully",
success_count, config.input_files.size()) << std::endl;
return success_count;
}
std::expected<int, Error> CliRunner::parse_format(const std::string& fmt_str) {
if (fmt_str == "png16") return static_cast<int>(OutputFormat::PNG_16bit);
if (fmt_str == "png8") return static_cast<int>(OutputFormat::PNG_8bit);
if (fmt_str == "tiff16") return static_cast<int>(OutputFormat::TIFF_16bit);
if (fmt_str == "jpeg") return static_cast<int>(OutputFormat::JPEG);
return std::unexpected(make_error(
ErrorCode::InvalidArgument,
std::format("Unknown output format: '{}'. Use: png16, png8, tiff16, jpeg", fmt_str)));
}
} // namespace photoconv