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/.
4.1 KiB
Effector
An image effects app: a GTK4 interface and a headless CLI over a set of pluggable effects. Effects are self-contained classes that declare their own parameters; the interface builds its widgets and the CLI builds its flags from those declarations, so a new effect needs no UI or CLI code.
Build
Requirements: a C++20 compiler (GCC 13+), CMake 3.20+, gtkmm-4.0 and
gdk-pixbuf-2.0 development packages. On Arch: gcc cmake ninja gtkmm-4.0.
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure
The binary lands at build/effector; cmake --install build --prefix ~/.local
installs it to ~/.local/bin.
Run
build/effector # GTK interface
build/effector --list-effects
build/effector --help
Headless rendering, no display needed:
build/effector -o out.png -s 1920x1080 \
--strip-height 8 --jitter 4 --seed 42 photo1.jpg photo2.png
Any option can also be passed when launching the interface; it becomes the starting state (images, canvas size, effect settings, seed).
Effects
scanlines
Fills the canvas top to bottom with horizontal strips cut at random positions from the source images. Strips are taken at native scale, cropped at a random horizontal offset, and tiled when the source is narrower than the canvas.
| Parameter | Flag | Meaning |
|---|---|---|
strip_height |
--strip-height |
Height in pixels of each extracted line |
jitter |
--jitter |
Random ± variation of that height (0 = uniform) |
keep_order |
--keep-order |
Cycle the images in order instead of picking randomly |
The seed makes a result reproducible: same sources, settings and seed give the
same image, on any machine — the generator (src/core/rng.hpp) is a hand-rolled
xoshiro256** rather than <random>, whose distributions are not portable.
Recreate draws a new seed.
Layout
src/core/ image buffer and IO, param specs, effect base, registry, rng, thread helper
src/effects/ one file per effect
src/cli/ argument parsing (flags generated from the registry)
src/gui/ gtkmm 4 interface
tests/ core checks, run by ctest
python-reference/ the original Python prototype, kept for reference only
effector_core is a GUI-free static library that links only gdk-pixbuf, so
headless rendering never touches GTK or a display. The GTK front end links on
top of it.
Adding an effect
- Create
src/effects/myeffect.{hpp,cpp}with a subclass ofEffect:
class MyEffect : public Effect {
std::string name() const override { return "myeffect"; }
std::string title() const override { return "My Effect"; }
std::string description() const override { return "..."; }
const std::vector<ParamSpec>& params() const override {
static const std::vector<ParamSpec> specs = {
ParamSpec::makeInt("amount", "Amount", 4, 1, 64, "How much."),
};
return specs;
}
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
const ParamValues& values) const override {
const long amount = paramInt(values, "amount", 4);
... // return a width x height RGB image
}
};
- Add it to
_EFFECT_CLASSES-style list insrc/core/registry.cppand to the source list inCMakeLists.txt.
Widgets and flags follow from params() automatically. Draw everything random
from the Rng you are handed so results stay reproducible, and use
parallelFor (src/core/parallel.hpp) for the pixel work.
Performance notes
The render loop first draws the whole strip plan from the seeded generator, then
copies the pixels in parallel across the hardware threads; each row is a
memcpy, and horizontal tiling doubles the filled span per pass. Source images
are decoded in parallel too, which is what dominates a headless run.
A 7680×4320 canvas at --strip-height 2 from three multi-megapixel JPEGs renders
in ~35 ms, against ~73 ms for the Python prototype; the interface reports the
render time of the current preview in its status line.