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>
89 lines
3.1 KiB
C++
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;
|
|
}
|
|
}
|