Files
effector/src/main.cpp
T
SiandClaude Opus 5 4739d39a51 Let effects declare whether they need source images
Whether an effect works from the loaded photographs is now part of the
declarative plug-in contract, next to params(): Effect::needsSources()
defaults to true, and blur — which only softens what it is handed — says
no and blurs an empty canvas when given nothing.

Only the first stage of a cascade ever sees the sources, so
chainNeedsSources() just asks the first stage. Both front-ends gate on
that instead of on "are there any images": the CLI only refuses an empty
batch when the chain wants one, and the interface renders a blur-led
chain with no images loaded, saying whichever thing is actually missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:49:16 +02:00

89 lines
3.1 KiB
C++

#include <unistd.h>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <mutex>
#include "cli/options.hpp"
#include "core/chain.hpp"
#include "core/effect.hpp"
#include "core/image.hpp"
#include "core/registry.hpp"
#include "core/rng.hpp"
#include "gui/app.hpp"
namespace {
int renderHeadless(const effector::Options& options) {
const auto stages = effector::buildChain(options);
std::vector<effector::LoadFailure> failures;
// Decoding a batch of photographs takes seconds; on a terminal keep a
// single line updated so the wait is not silent. Redirected output stays
// clean, since the line would only clutter a log.
effector::LoadProgress progress;
std::mutex progressMutex;
const bool showProgress = isatty(fileno(stderr)) != 0 && options.images.size() > 1;
if (showProgress) {
progress.onFile = [&](std::size_t done, std::size_t total, const std::string& path) {
std::lock_guard<std::mutex> lock(progressMutex);
std::cerr << "\r\033[Kreading " << done << "/" << total << " "
<< std::filesystem::path(path).filename().string() << std::flush;
};
}
auto sources = effector::loadImages(options.images, failures, showProgress ? &progress : nullptr);
if (showProgress) std::cerr << "\r\033[K" << std::flush;
for (const auto& failure : failures) {
std::cerr << "effector: skipping " << failure.path << ": " << failure.message << "\n";
}
if (sources.empty() && effector::chainNeedsSources(stages)) {
std::cerr << "effector: no usable source images\n";
return 2;
}
const std::uint64_t seed = options.seed.value_or(effector::Rng::randomSeed());
effector::Rng rng(seed);
const auto start = std::chrono::steady_clock::now();
const auto image =
effector::renderChain(stages, sources, options.width, options.height, rng);
const auto renderMillis =
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
image.save(options.output);
std::cout << options.output << " " << options.width << "x" << options.height
<< " effect=" << effector::chainSlug(stages) << " seed=" << seed << " render="
<< std::fixed << std::setprecision(1) << renderMillis << " ms\n";
return 0;
}
} // namespace
int main(int argc, char** argv) {
try {
const auto options = effector::parseOptions(argc, argv);
if (options.showHelp) {
std::cout << effector::usage(argv[0]);
return 0;
}
if (options.listEffects) {
for (const auto& effect : effector::effects()) {
std::cout << effect->name() << "\t" << effect->title() << " - "
<< effect->description() << "\n";
}
return 0;
}
if (!options.output.empty()) return renderHeadless(options);
return effector::runGui(options, argc, argv);
} catch (const std::exception& error) {
std::cerr << "effector: " << error.what() << "\n";
return 1;
}
}