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>
This commit is contained in:
Si
2026-09-18 15:49:16 +02:00
co-authored by Claude Opus 5
parent a82fba7886
commit 4739d39a51
10 changed files with 112 additions and 48 deletions
+9 -9
View File
@@ -100,7 +100,6 @@ void blurVertical(const Image& src, Image& dst, int radius) {
Image BlurEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
const ParamValues& values) const {
if (sources.empty()) throw EffectError("No source images.");
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
const int radius = static_cast<int>(std::max(1L, paramInt(values, "radius", 8)));
@@ -108,15 +107,16 @@ Image BlurEffect::render(const std::vector<Image>& sources, int width, int heigh
const std::string direction = paramChoice(values, "direction", "both");
const bool firstSource = paramBool(values, "first_source", false);
// Every random value is drawn here, before any threading, so that a seed
// always gives the same image.
const std::size_t index = firstSource ? 0 : rng.below(sources.size() - 1);
const Image& src = sources[index];
const int sx = static_cast<int>(rng.inRange(0, std::max(0, src.width() - width)));
const int sy = static_cast<int>(rng.inRange(0, std::max(0, src.height() - height)));
Image canvas(width, height);
fillFromSource(canvas, src, sx, sy);
if (!sources.empty()) {
// Every random value is drawn here, before any threading, so that a
// seed always gives the same image.
const std::size_t index = firstSource ? 0 : rng.below(sources.size() - 1);
const Image& src = sources[index];
const int sx = static_cast<int>(rng.inRange(0, std::max(0, src.width() - width)));
const int sy = static_cast<int>(rng.inRange(0, std::max(0, src.height() - height)));
fillFromSource(canvas, src, sx, sy);
}
Image scratch(width, height);
for (int pass = 0; pass < passes; ++pass) {