#include "cli/options.hpp" #include #include #include #include #include #include "core/canvas.hpp" #include "core/chain.hpp" #include "core/effect.hpp" #include "core/registry.hpp" namespace effector { namespace { bool parseLong(const std::string& text, long& out) { const char* begin = text.data(); const char* end = begin + text.size(); const auto result = std::from_chars(begin, end, out); return result.ec == std::errc{} && result.ptr == end; } bool parseSize(const std::string& text, int& width, int& height) { const auto sep = text.find_first_of("xX*,"); if (sep == std::string::npos) return false; long w = 0; long h = 0; if (!parseLong(text.substr(0, sep), w) || !parseLong(text.substr(sep + 1), h)) return false; // Checked as longs, before the narrowing: 4294968296 cast straight to int // is 1000, which would render a plausible canvas nobody asked for. if (w < kMinCanvasSide || w > kMaxCanvasSide) return false; if (h < kMinCanvasSide || h > kMaxCanvasSide) return false; width = static_cast(w); height = static_cast(h); return true; } // Every effect's params share one flag namespace, exactly as in the interface. const ParamSpec* findSpec(const std::string& flag) { for (const auto& effect : effects()) { for (const auto& spec : effect->params()) { if (spec.flag() == flag) return &spec; } } return nullptr; } std::string requireValue(int argc, char** argv, int& i, const std::string& flag) { if (i + 1 >= argc) throw EffectError(flag + " needs a value"); return argv[++i]; } } // namespace Options parseOptions(int argc, char** argv) { Options options; for (int i = 1; i < argc; ++i) { const std::string arg = argv[i]; if (arg == "-h" || arg == "--help") { options.showHelp = true; } else if (arg == "--list-effects") { options.listEffects = true; } else if (arg == "-e" || arg == "--effect") { const std::string name = requireValue(argc, argv, i, arg); effectByName(name); // validate now, fail early options.effects.push_back(name); } else if (arg == "-o" || arg == "--output") { options.output = requireValue(argc, argv, i, arg); } else if (arg == "-s" || arg == "--size") { const std::string value = requireValue(argc, argv, i, arg); if (!parseSize(value, options.width, options.height)) { throw EffectError("invalid size '" + value + "', expected e.g. 1920x1080 with " "each side between " + std::to_string(kMinCanvasSide) + " and " + std::to_string(kMaxCanvasSide)); } } else if (arg == "--format") { const std::string value = requireValue(argc, argv, i, arg); if (canvasFormatByKey(value) == nullptr) { throw EffectError("unknown canvas format '" + value + "', see --help"); } options.format = value; } else if (arg == "--dpi") { const std::string value = requireValue(argc, argv, i, arg); long dpi = 0; if (!parseLong(value, dpi) || dpi < kMinDpi || dpi > kMaxDpi) { throw EffectError("invalid dpi '" + value + "', expected " + std::to_string(kMinDpi) + "-" + std::to_string(kMaxDpi)); } options.dpi = static_cast(dpi); } else if (arg == "--disable") { const std::string name = requireValue(argc, argv, i, arg); effectByName(name); // validate now, exactly as -e does options.disabled.push_back(name); } else if (arg == "--seed") { const std::string value = requireValue(argc, argv, i, arg); long seed = 0; if (!parseLong(value, seed)) throw EffectError("invalid seed '" + value + "'"); options.seed = static_cast(seed); } else if (arg.rfind("--no-", 0) == 0) { const std::string flag = "--" + arg.substr(5); const ParamSpec* spec = findSpec(flag); if (spec == nullptr || spec->type != ParamSpec::Type::Bool) { throw EffectError("unknown option '" + arg + "'"); } options.params[spec->key] = false; } else if (arg.rfind("--", 0) == 0) { const ParamSpec* spec = findSpec(arg); if (spec == nullptr) throw EffectError("unknown option '" + arg + "'"); switch (spec->type) { case ParamSpec::Type::Bool: options.params[spec->key] = true; break; case ParamSpec::Type::Int: { long value = 0; const std::string text = requireValue(argc, argv, i, arg); if (!parseLong(text, value)) { throw EffectError(arg + ": '" + text + "' is not a whole number"); } options.params[spec->key] = value; break; } case ParamSpec::Type::Double: { const std::string text = requireValue(argc, argv, i, arg); try { options.params[spec->key] = std::stod(text); } catch (const std::exception&) { throw EffectError(arg + ": '" + text + "' is not a number"); } break; } case ParamSpec::Type::Path: case ParamSpec::Type::Text: options.params[spec->key] = requireValue(argc, argv, i, arg); break; case ParamSpec::Type::Color: { const std::string text = requireValue(argc, argv, i, arg); std::uint8_t rgb[3]; if (!parseColor(text, rgb)) { throw EffectError(arg + ": '" + text + "' is not a colour, expected e.g. #ff8800"); } options.params[spec->key] = formatColor(rgb); break; } case ParamSpec::Type::Choice: { const std::string text = requireValue(argc, argv, i, arg); bool valid = false; for (const auto& choice : spec->choices) valid = valid || choice == text; if (!valid) throw EffectError(arg + ": unknown value '" + text + "'"); options.params[spec->key] = text; break; } } } else if (arg.rfind("-", 0) == 0 && arg.size() > 1) { throw EffectError("unknown option '" + arg + "'"); } else { options.images.push_back(arg); } } // Applied once everything is parsed, so --dpi may come before --format and // so a format always wins over an -s given alongside it. if (const auto* format = canvasFormatByKey(options.format)) { canvasFormatSize(*format, options.dpi, options.width, options.height); } return options; } std::string usage(const char* programName) { std::ostringstream out; out << "Usage: " << programName << " [options] [image ...]\n\n" << "Image effects app. Runs the GTK interface unless --output is given.\n\n" << "Options:\n" << " -e, --effect NAME effect to apply, repeat to cascade (--output" " defaults to " << defaultEffectName() << ")\n" << " -o, --output FILE render headlessly to this file and exit\n" << " -s, --size WxH canvas size, each side " << kMinCanvasSide << "-" << kMaxCanvasSide << " (default: 1920x1080)\n" << " --format NAME canvas format, overrides --size (see below)\n" << " --dpi N resolution of a print format, " << kMinDpi << "-" << kMaxDpi << " (default: " << kDefaultDpi << ")\n" << " --disable NAME keep this effect in the chain but skip it\n" << " --seed N random seed (default: random)\n" << " --list-effects list available effects and exit\n" << " -h, --help show this help and exit\n"; out << "\nCanvas formats (--format):\n"; for (const auto& format : canvasFormats()) { out << " " << format.key; if (format.key.size() < 20) out << std::string(20 - format.key.size(), ' '); out << " " << format.label << ", " << canvasFormatDetail(format) << (format.print ? " at --dpi" : "") << "\n"; } for (const auto& effect : effects()) { out << "\n" << effect->title() << " options:\n"; for (const auto& spec : effect->params()) { std::string flag = spec.flag(); if (spec.type != ParamSpec::Type::Bool) flag += " VALUE"; out << " " << flag; if (flag.size() < 20) out << std::string(20 - flag.size(), ' '); out << " " << (spec.help.empty() ? spec.label : spec.help) << "\n"; } } return out.str(); } std::vector buildChain(const Options& options) { std::vector stages; for (const auto& name : options.effects) { ChainStage stage; stage.effect = effectByName(name); stage.values = stage.effect->defaults(); stage.enabled = std::find(options.disabled.begin(), options.disabled.end(), name) == options.disabled.end(); for (const auto& spec : stage.effect->params()) { const auto it = options.params.find(spec.key); if (it != options.params.end()) stage.values[spec.key] = it->second; } stages.push_back(std::move(stage)); } return stages; } } // namespace effector