packaging/arch/PKGBUILD builds the tagged release from this repository and installs it with pacman; the version test keeps its pkgver in step with CMakeLists.txt. A desktop entry puts the interface in the app menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
6.2 KiB
Markdown
162 lines
6.2 KiB
Markdown
# 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`.
|
||
|
||
```bash
|
||
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`.
|
||
|
||
On Arch, `packaging/arch/PKGBUILD` builds the release tagged `v<version>` from
|
||
this repository and installs it with pacman, desktop entry included:
|
||
|
||
```bash
|
||
cd packaging/arch && makepkg -si
|
||
```
|
||
|
||
## Run
|
||
|
||
```bash
|
||
build/effector # GTK interface
|
||
build/effector --list-effects
|
||
build/effector --help
|
||
build/effector --version
|
||
```
|
||
|
||
Headless rendering, no display needed:
|
||
|
||
```bash
|
||
build/effector -o out.png -s 1920x1080 \
|
||
--strip-height 8 --jitter 4 --seed 42 photo1.jpg photo2.png
|
||
```
|
||
|
||
The canvas is either a size in pixels (`-s 1920x1080`, the Custom entry in the
|
||
interface) or one of the standard formats, which `--help` lists in full:
|
||
|
||
```bash
|
||
build/effector -o post.png --format instagram-post -e tiles
|
||
build/effector -o sheet.png --format a4 --dpi 300 -e scanlines
|
||
```
|
||
|
||
A web format — the Instagram sizes, Full HD, 4K UHD — is a fixed pixel size. A
|
||
print format — A5, A4, A3, US Letter — is a sheet of paper, and becomes pixels
|
||
at `--dpi` (36–1200, 300 by default), so A4 at 300 dpi is 2480×3508. Picking a
|
||
format in the interface fills the width and height in and greys them out;
|
||
Custom hands them back, keeping whatever the format last worked out to. A
|
||
format is saved with the project, so an A4 project reopens as A4 rather than as
|
||
the pixels it happened to be.
|
||
|
||
Any option can also be passed when launching the interface; it becomes the
|
||
starting state (images, canvas size, effect settings, seed).
|
||
|
||
Reading a batch of large photographs takes seconds, so it never blocks the
|
||
interface: decoding runs in the background and a progress window shows how many
|
||
files are done, which one is being read, and a Cancel button that stops the
|
||
batch. Small batches finish before the window would be drawn, so it only appears
|
||
if the decode is still going after 250 ms. On a terminal the headless mode keeps
|
||
a single `reading n/total` line updated on stderr; redirected output stays clean.
|
||
|
||
## Versions
|
||
|
||
Releases are listed in [CHANGELOG.md](CHANGELOG.md) and tagged `vX.Y.Z`
|
||
(`git tag -l 'v*'`). `effector --version` prints the version, and the menu in
|
||
the interface has Changelog and About Effector.
|
||
|
||
To cut a release: bump the `project()` version in `CMakeLists.txt`, move the
|
||
changes into a new `## [X.Y.Z] - date` entry at the top of `CHANGELOG.md` (the
|
||
core tests fail until the two agree), commit, and tag it:
|
||
`git tag -a vX.Y.Z -m "Effector X.Y.Z"`.
|
||
|
||
## 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
|
||
|
||
1. Create `src/effects/myeffect.{hpp,cpp}` with a subclass of `Effect`:
|
||
|
||
```cpp
|
||
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
|
||
}
|
||
};
|
||
```
|
||
|
||
2. Add it to `_EFFECT_CLASSES`-style list in `src/core/registry.cpp` and to the
|
||
source list in `CMakeLists.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.
|
||
|
||
Decoding threads take the next file from a shared cursor rather than a fixed
|
||
slice of the list, so one very large file cannot stall the files queued behind
|
||
it and progress advances at a steady rate.
|