Effector: C++/CMake image effects app
Port of the Python prototype to C++20 with a CMake build: - effector_core: GUI-free static library (effects, image IO via gdk-pixbuf, declarative param specs, registry, portable seeded RNG, thread helper) - scanlines effect: plans strips from the seeded RNG, then copies pixels in parallel with memcpy and doubling horizontal tiling - gtkmm 4 interface: native file dialogs, widgets generated from the param specs, coalesced background renders, preview as a GDK memory texture - CLI: flags generated from the effect registry, headless rendering with no display required - ctest suite covering the RNG, registry, effect geometry and image round trip The Python prototype is kept under python-reference/.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
#include "cli/options.hpp"
|
||||
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
#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;
|
||||
if (w <= 0 || h <= 0) return false;
|
||||
width = static_cast<int>(w);
|
||||
height = static_cast<int>(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;
|
||||
options.effect = defaultEffectName();
|
||||
|
||||
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") {
|
||||
options.effect = requireValue(argc, argv, i, arg);
|
||||
effectByName(options.effect); // validate now, fail early
|
||||
} 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");
|
||||
}
|
||||
} 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<std::uint64_t>(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::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);
|
||||
}
|
||||
}
|
||||
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 (default: " << defaultEffectName() << ")\n"
|
||||
<< " -o, --output FILE render headlessly to this file and exit\n"
|
||||
<< " -s, --size WxH canvas size (default: 1920x1080)\n"
|
||||
<< " --seed N random seed (default: random)\n"
|
||||
<< " --list-effects list available effects and exit\n"
|
||||
<< " -h, --help show this help and exit\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();
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user