#include "CliRunner.h" #include "../converter/pipeline/ImageData.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 #include #include namespace photoconv { // ───────────────────────────────────────────────────────────────────────────── // parse_args // ───────────────────────────────────────────────────────────────────────────── std::expected CliRunner::parse_args(int argc, char* argv[]) { Config config; bool reading_inputs = false; for (int i = 1; i < argc; ++i) { const std::string arg{argv[i]}; if (arg == "--cli") { reading_inputs = false; continue; } if (arg == "--batch") { config.batch_mode = true; reading_inputs = false; continue; } if (arg == "--config") { reading_inputs = false; if (i + 1 < argc) { config.config_file = argv[++i]; config.batch_mode = true; // --config implies batch mode } else { return std::unexpected(make_error( ErrorCode::InvalidArgument, "--config requires a file path")); } continue; } 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, "--output requires a directory path")); } continue; } if (arg == "--format") { reading_inputs = false; if (i + 1 < argc) { config.output_format = argv[++i]; } else { return std::unexpected(make_error( ErrorCode::InvalidArgument, "--format requires a value (png16|png8|tiff16|jpeg)")); } continue; } if (arg == "--quality") { reading_inputs = false; if (i + 1 < argc) { config.jpeg_quality = std::stoi(argv[++i]); } else { return std::unexpected(make_error( ErrorCode::InvalidArgument, "--quality requires a numeric value")); } continue; } if (arg == "-v" || arg == "--verbose") { config.verbose = true; reading_inputs = false; continue; } if (arg == "-h" || arg == "--help") { std::cout << "Usage: photo-converter --cli [options] -i \n" " photo-converter --batch --config config.ini\n" "\n" "Options:\n" " --cli CLI mode (no GUI)\n" " --batch Batch mode (read config for input directory)\n" " --config INI configuration file (implies --batch)\n" " -i, --input Input image files (RAW or standard)\n" " -o, --output Output directory (default: output/)\n" " --format 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 is active or the argument does not start with '-', // treat it as an input file path. if (reading_inputs || (!arg.empty() && arg.front() != '-')) { config.input_files.emplace_back(arg); reading_inputs = true; } } // Validation: we need either input files or a config file for batch mode. if (!config.batch_mode && config.input_files.empty()) { return std::unexpected(make_error( ErrorCode::InvalidArgument, "No input files specified. Use -i or --batch --config config.ini")); } return config; } // ───────────────────────────────────────────────────────────────────────────── // run // ───────────────────────────────────────────────────────────────────────────── std::expected CliRunner::run(const Config& config) const { // ── Resolve AppConfig ──────────────────────────────────────────────────── AppConfig app_cfg{}; if (!config.config_file.empty()) { auto cfg_result = AppConfig::load(config.config_file); if (!cfg_result.has_value()) { return std::unexpected(cfg_result.error()); } app_cfg = std::move(cfg_result.value()); std::cout << std::format("[CLI] Loaded config: {}", config.config_file.string()) << std::endl; } // CLI flags override config file values where set explicitly. if (!config.output_dir.empty()) { app_cfg.batch.output_dir = config.output_dir; } if (!config.output_format.empty() && config.output_format != "png16") { // "png16" is the default; only override if the user explicitly changed it. app_cfg.conversion.output_format = config.output_format; } if (config.jpeg_quality != 95) { app_cfg.quality.jpeg_quality = config.jpeg_quality; } // ── Collect input files ────────────────────────────────────────────────── std::vector files = config.input_files; if (files.empty() && config.batch_mode) { const auto extensions = app_cfg.parsed_extensions(); files = collect_files(app_cfg.batch.input_dir, extensions, app_cfg.batch.recursive); if (files.empty()) { std::cerr << std::format( "[CLI] No matching files found in: {}", app_cfg.batch.input_dir.string()) << std::endl; return 0; } } // ── Build pipeline ─────────────────────────────────────────────────────── RawLoader loader; Pipeline pipeline = build_pipeline(app_cfg); // ── Process files ──────────────────────────────────────────────────────── int success_count = 0; const auto total = static_cast(files.size()); for (int idx = 0; idx < total; ++idx) { const auto& file = files[static_cast(idx)]; std::cerr << std::format("[{}/{}] Processing {}...", idx + 1, total, file.filename().string()) << std::endl; // Load image 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; } // Run pipeline 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: {}/{} file(s) converted successfully", success_count, total) << std::endl; return success_count; } // ───────────────────────────────────────────────────────────────────────────── // collect_files // ───────────────────────────────────────────────────────────────────────────── std::vector CliRunner::collect_files( const std::filesystem::path& dir, const std::vector& extensions, const bool recursive) { std::vector result; if (!std::filesystem::is_directory(dir)) { std::cerr << std::format("[CLI] Input directory not found: {}", dir.string()) << std::endl; return result; } auto collect_entry = [&](const std::filesystem::directory_entry& entry) { if (!entry.is_regular_file()) return; // Compare extension case-insensitively. std::string ext = entry.path().extension().string(); std::ranges::transform(ext, ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); const bool match = std::ranges::any_of(extensions, [&ext](const std::string& e) { return e == ext; }); if (match) { result.push_back(entry.path()); } }; if (recursive) { for (const auto& entry : std::filesystem::recursive_directory_iterator{dir}) { collect_entry(entry); } } else { for (const auto& entry : std::filesystem::directory_iterator{dir}) { collect_entry(entry); } } std::ranges::sort(result); return result; } // ───────────────────────────────────────────────────────────────────────────── // build_pipeline // ───────────────────────────────────────────────────────────────────────────── Pipeline CliRunner::build_pipeline(const AppConfig& app_cfg) { Pipeline pipeline; // Resolve forced film type from config. // "auto" → Unknown (auto-detection), "c41" → ColorNegative, "bw" → BWNegative. FilmType forced_film = FilmType::Unknown; if (app_cfg.conversion.film_type == "c41") { forced_film = FilmType::ColorNegative; } else if (app_cfg.conversion.film_type == "bw") { forced_film = FilmType::BWNegative; } pipeline.add_stage(std::make_unique()); pipeline.add_stage(std::make_unique(forced_film)); if (app_cfg.conversion.invert) { pipeline.add_stage(std::make_unique()); } pipeline.add_stage(std::make_unique()); pipeline.add_stage(std::make_unique()); pipeline.add_stage(std::make_unique(OutputConfig{ app_cfg.batch.output_dir, app_cfg.output_format(), app_cfg.quality.jpeg_quality })); return pipeline; } } // namespace photoconv