Files
effector/python-reference/effector/cli.py
T
hans 925a719da0 Effector: C++/CMake image effects app
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/.
2026-09-18 14:28:13 +02:00

111 lines
4.0 KiB
Python

"""Command line entry point: launches the GUI, or renders headlessly."""
from __future__ import annotations
import argparse
import random
import sys
from .effects import DEFAULT_EFFECT, EffectError, get_effect, list_effects, load_sources
from .params import BoolParam, ChoiceParam, FloatParam, IntParam
def parse_size(text: str) -> tuple[int, int]:
for sep in ("x", "X", "*", ","):
if sep in text:
a, _, b = text.partition(sep)
try:
w, h = int(a), int(b)
except ValueError:
break
if w > 0 and h > 0:
return w, h
break
raise argparse.ArgumentTypeError(f"invalid size {text!r}, expected e.g. 1920x1080")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="effector",
description="Image effects app. Runs the GTK interface unless --output is given.",
)
parser.add_argument("images", nargs="*", help="source image files")
parser.add_argument(
"-e", "--effect", default=DEFAULT_EFFECT, choices=sorted(e.name for e in list_effects())
)
parser.add_argument("-o", "--output", help="render headlessly to this file and exit")
parser.add_argument(
"-s", "--size", type=parse_size, default=(1920, 1080), help="canvas size, e.g. 1920x1080"
)
parser.add_argument("--seed", type=int, help="random seed (default: random)")
parser.add_argument(
"--list-effects", action="store_true", help="list available effects and exit"
)
# Effect-specific flags, generated from the declarative param specs. Every
# effect's params share one namespace; defaults stay None so that the
# effect's own default applies when a flag is not given.
seen: set[str] = set()
for cls in list_effects():
group = parser.add_argument_group(f"{cls.title} options")
for p in cls.params:
if p.key in seen:
continue
seen.add(p.key)
help_text = p.help or p.label
if isinstance(p, BoolParam):
group.add_argument(p.flag, dest=p.key, action="store_true", default=None,
help=help_text)
group.add_argument("--no-" + p.flag[2:], dest=p.key, action="store_false",
default=None, help=argparse.SUPPRESS)
elif isinstance(p, IntParam):
group.add_argument(p.flag, dest=p.key, type=int, default=None, help=help_text)
elif isinstance(p, FloatParam):
group.add_argument(p.flag, dest=p.key, type=float, default=None, help=help_text)
elif isinstance(p, ChoiceParam):
group.add_argument(p.flag, dest=p.key, choices=list(p.choices), default=None,
help=help_text)
return parser
def render_headless(args) -> int:
effect = get_effect(args.effect)
images, failures = load_sources(args.images)
for path, message in failures:
print(f"effector: skipping {path}: {message}", file=sys.stderr)
if not images:
print("effector: no usable source images", file=sys.stderr)
return 2
seed = args.seed if args.seed is not None else random.randrange(2**32)
width, height = args.size
values = {p.key: getattr(args, p.key, None) for p in effect.params}
image = effect.render(images, width, height, random.Random(seed), **values)
image.save(args.output)
print(f"{args.output} {width}x{height} effect={effect.name} seed={seed}")
return 0
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if args.list_effects:
for cls in list_effects():
print(f"{cls.name}\t{cls.title} - {cls.description}")
return 0
try:
if args.output:
return render_headless(args)
from .gui.app import run_gui
return run_gui(args)
except EffectError as exc:
print(f"effector: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())