Watermark and Border module #508
Replies: 2 comments 3 replies
|
Hi, |
|
So, here's a guide on how to add a new tool, courtesy of Claude. Hope it helps. Adding a New Tool to ARTThis guide walks through every file you must touch to add a new image-processing Throughout, we use a hypothetical tool called Foo with a member name Overview — the moving partsA tool touches four layers, in this order:
A complete checklist is at the end. 1. Parameters1.1 Declare the struct —
|
| Stage | Contains (examples) | Typical use |
|---|---|---|
STAGE_0 |
dehaze, dynamic range compression | very early, scene-referred |
STAGE_1 |
channel mixer, exposure, HSL, tone equalizer | linear RGB, pre-tone |
STAGE_2 |
sharpening, denoise, defringe, color correction | detail / local |
STAGE_3 |
grain, log encoding, saturation, curves, B&W | creative / display-referred |
Insert your STEP_ between the neighbours whose "before/after" ordering you
want. For a bool (stop) tool, follow the stop = stop || STEP_s_(foo);
pattern and place it where the guarding if (!stop) logic makes sense.
Optionally bump NUM_PIPELINE_STEPS (a constant near the top of the same file)
so the progress bar stays accurate.
You do not need to edit dcrop.cc, simpleprocess.cc, or
improccoordinator.cc — all three call process() generically, so a tool added
to a stage automatically runs in the interactive preview, the batch/export
output, and the navigator.
3. Events (making a GUI change re-process)
When the user changes a widget, the panel calls
listener->panelChanged(EvSomething, description). The engine looks up which
part of the pipeline that event affects and re-runs only what is needed. ART
creates these events at runtime via the event mapper — you do not edit
the static procevents.h enum or the refreshmap.cc array for a new tool.
In your panel's constructor (see §4) you request events:
#include "eventmapper.h"
...
auto m = ProcEventMapper::getInstance();
EvEnabled = m->newEvent(rtengine::RGBCURVE, "HISTORY_MSG_FOO_ENABLED");
EvAmount = m->newEvent(rtengine::RGBCURVE, "HISTORY_MSG_FOO_AMOUNT");
EvMode = m->newEvent(rtengine::RGBCURVE, "HISTORY_MSG_FOO_MODE");The first argument (the "action") is a refresh level from
rtengine/refreshmap.h. It determines which pipeline stages re-run. The
mapping between refresh masks and stages is:
M_RGBCURVE→ re-runs fromSTAGE_1M_LUMACURVE→ re-runs fromSTAGE_2M_LUMINANCE | M_COLOR→ re-runsSTAGE_3only
Pick the action constant matching the earliest stage your tool needs
re-run. Convenient pre-defined constants (all in refreshmap.h):
| Constant | Re-runs from | Use for a tool in |
|---|---|---|
RGBCURVE |
STAGE_1 | STAGE_1 / STAGE_2 / STAGE_3 |
SHARPENING |
STAGE_2 | STAGE_2 / STAGE_3 |
LUMINANCECURVE |
STAGE_3 | STAGE_3 |
DIRPYREQUALIZER |
STAGE_3 | STAGE_3 |
TRANSFORM |
geometry | geometry tools |
For our foo tool placed in STAGE_3, LUMINANCECURVE (or the equivalent
DIRPYREQUALIZER, which Film Grain uses) is the minimal correct choice;
RGBCURVE also works but re-runs more than necessary. The second argument is the
name of a history-message translation key (see §5) shown in the History
panel; pass "" if you don't want a history entry.
Under the hood, ProcEventMapper::newEvent calls
RefreshMapper::getInstance()->newEvent() for a fresh event id and
mapEvent(event, action) to record its refresh level — so the action you pass
here is the refresh-map entry. No static table edit is required.
4. GUI
4.1 The panel — new files rtgui/foo.h / rtgui/foo.cc
Model on rtgui/grain.*. The panel inherits ToolParamBlock (the widget
container), FoldableToolPanel (the collapsible expander + enable checkbox +
reset button + the read/write/setDefaults virtuals), and any listener
interfaces for the widgets it uses (AdjusterListener for sliders).
rtgui/foo.h:
#pragma once
#include "adjuster.h"
#include "toolpanel.h"
#include <gtkmm.h>
class Foo: public ToolParamBlock,
public AdjusterListener,
public FoldableToolPanel {
public:
Foo();
void read(const rtengine::procparams::ProcParams *pp) override;
void write(rtengine::procparams::ProcParams *pp) override;
void setDefaults(const rtengine::procparams::ProcParams *defParams) override;
void adjusterChanged(Adjuster *a, double newval) override;
void adjusterAutoToggled(Adjuster *a, bool newval) override {}
void enabledChanged() override;
void modeChanged();
void toolReset(bool to_initial) override;
private:
Adjuster *amount;
MyComboBoxText *mode;
rtengine::ProcEvent EvEnabled;
rtengine::ProcEvent EvAmount;
rtengine::ProcEvent EvMode;
rtengine::procparams::FooParams initial_params;
};rtgui/foo.cc:
#include "foo.h"
#include "eventmapper.h"
using namespace rtengine;
using namespace rtengine::procparams;
Foo::Foo():
FoldableToolPanel(this, "foo", M("TP_FOO_LABEL"), false, true, true)
// | | |
// need11 ---+ | |
// useEnabled (on/off) ---+ |
// useReset (reset button) ----+
{
auto m = ProcEventMapper::getInstance();
EvEnabled = m->newEvent(rtengine::RGBCURVE, "HISTORY_MSG_FOO_ENABLED");
EvAmount = m->newEvent(rtengine::RGBCURVE, "HISTORY_MSG_FOO_AMOUNT");
EvMode = m->newEvent(rtengine::RGBCURVE, "HISTORY_MSG_FOO_MODE");
EvToolReset.set_action(rtengine::RGBCURVE);
amount = Gtk::manage(new Adjuster(M("TP_FOO_AMOUNT"), 0., 100., 1., 0.));
amount->setAdjusterListener(this);
amount->show();
Gtk::HBox *hb = Gtk::manage(new Gtk::HBox());
hb->pack_start(*Gtk::manage(new Gtk::Label(M("TP_FOO_MODE") + ": ")),
Gtk::PACK_SHRINK);
mode = Gtk::manage(new MyComboBoxText());
mode->append(M("TP_FOO_MODE_LINEAR"));
mode->append(M("TP_FOO_MODE_LOG"));
mode->signal_changed().connect(sigc::mem_fun(*this, &Foo::modeChanged));
hb->pack_start(*mode);
hb->show();
pack_start(*amount);
pack_start(*hb);
}
void Foo::read(const ProcParams *pp)
{
disableListener(); // block callbacks while loading
setEnabled(pp->foo.enabled);
amount->setValue(pp->foo.amount);
mode->set_active(int(pp->foo.mode));
enableListener();
}
void Foo::write(ProcParams *pp)
{
pp->foo.enabled = getEnabled();
pp->foo.amount = amount->getValue();
pp->foo.mode = FooParams::Mode(mode->get_active_row_number());
}
void Foo::setDefaults(const ProcParams *defParams)
{
amount->setDefault(defParams->foo.amount);
initial_params = defParams->foo; // saved for toolReset
}
void Foo::adjusterChanged(Adjuster *a, double newval)
{
if (listener && getEnabled()) {
if (a == amount) {
listener->panelChanged(EvAmount, a->getTextValue());
}
}
}
void Foo::modeChanged()
{
if (listener && getEnabled()) {
listener->panelChanged(EvMode, mode->get_active_text());
}
}
void Foo::enabledChanged()
{
if (listener) {
if (get_inconsistent()) {
listener->panelChanged(EvEnabled, M("GENERAL_UNCHANGED"));
} else if (getEnabled()) {
listener->panelChanged(EvEnabled, M("GENERAL_ENABLED"));
} else {
listener->panelChanged(EvEnabled, M("GENERAL_DISABLED"));
}
}
}
void Foo::toolReset(bool to_initial)
{
ProcParams pp;
if (to_initial) {
pp.foo = initial_params;
}
pp.foo.enabled = getEnabled();
read(&pp);
}Key rules:
read()copies params → widgets; wrap it indisableListener()/
enableListener()so it doesn't fire change events.write()copies widgets → params. It must be the exact inverse ofread().adjusterChanged/modeChangedfirelistener->panelChanged(EvXxx, text),
guarded byif (listener && getEnabled()).listeneris the coordinator,
wired up automatically (§4.2).setDefaultsstashesinitial_paramsfor the reset button;toolReset
restores it.
4.2 Register with the coordinator — rtgui/toolpanelcoord.*
In rtgui/toolpanelcoord.h:
- Add the include near the other tool includes:
#include "foo.h"
- Add a member pointer in the
protectedblock with the other panels:Foo *foo;
In rtgui/toolpanelcoord.cc, in the constructor:
- Allocate it with the other
Gtk::manage(new ...)calls:foo = Gtk::manage(new Foo()); - Add it to a tab with
addfavoritePanel(<panel>, foo);. The tab VBoxes are
exposurePanel,detailsPanel,colorPanel,transformPanel,rawPanel,
localPanel,effectsPanel. For example, to put Foo on the Effects tab:To nest it under another tool, pass that tool'saddfavoritePanel(effectsPanel, foo);getPackBox()and a level:
addfavoritePanel(lenspanel->getPackBox(), foo, 2);.
That is the entire registration. read(), write(), setDefaults(), the
editor provider, and the fold-state handling all iterate the internal
toolPanels / expList vectors, which addfavoritePanel → addPanel
populate — so there is nothing else to wire. The line
for (auto toolPanel : toolPanels) toolPanel->setListener(this); later in the
constructor connects your panel's listener to the coordinator automatically.
4.3 Partial paste dialog — rtgui/partialpastedlg.cc
The dialog is fully table-driven. Add one row to the get_toggles(...) table,
in whichever group column fits (the index is the group: 0 Exposure, 1 Detail,
2 Effects, 3 Color, 4 Lens, 5 Composition, 6 Local, 7 Raw, 8 Meta). For a plain
bool flag, pass its address as the third field and nullptr as the last:
{"PARTIALPASTE_FOO", &pedited.foo, 2, nullptr},(Only the four tri-state mask tools use the last unsigned* field instead; a
plain tool leaves it nullptr and passes &pedited.foo as the bool*.) The
first field, "PARTIALPASTE_FOO", is a translation key (§5).
5. Localization
All UI text goes through the M("KEY") macro, which looks the key up in the
language files under rtdata/languages/. The canonical file is
rtdata/languages/default (English); every key you reference must exist there.
Add entries for every string you introduced:
HISTORY_MSG_FOO_ENABLED;Foo
HISTORY_MSG_FOO_AMOUNT;Foo - Amount
HISTORY_MSG_FOO_MODE;Foo - Mode
PARTIALPASTE_FOO;Foo
TP_FOO_LABEL;Foo
TP_FOO_AMOUNT;Amount
TP_FOO_MODE;Mode
TP_FOO_MODE_LINEAR;Linear
TP_FOO_MODE_LOG;Log
Convention: TP_* for tool-panel labels, HISTORY_MSG_* for History-panel
descriptions, PARTIALPASTE_* for the paste dialog. The format is
KEY;Text, one per line, kept sorted. You only need to add to default;
translators fill in the other language files, and missing keys fall back to the
default text.
6. Build
Add each new .cc (headers are picked up implicitly) to the appropriate
CMakeLists.txt:
rtgui/CMakeLists.txt— addfoo.ccto theNONCLISOURCEFILESset.rtengine/CMakeLists.txt— addipfoo.ccto the engine source list.
Then rebuild. A .arp profile written by the new build will carry a [Foo]
group; older builds ignore unknown groups, and your load code ignores missing
ones, so the format stays forward/backward compatible.
Checklist
Parameters:
-
rtengine/procparams.h—struct FooParams { ... }+FooParams foo;member. -
rtengine/procparams.cc— constructor (defaults),operator==,operator!=. -
rtengine/procparams.cc—setDefaults():foo = FooParams();. -
rtengine/procparams.cc—save():if (RELEVANT_(foo)) { saveToKeyfile(...); }. -
rtengine/procparams.cc—load():if (has_group("Foo") && RELEVANT_(foo)) { assignFromKeyfile(...); }. -
rtengine/procparams.cc—operator==: addppEQ_(foo) &&. -
rtgui/paramsedited.h—bool foo;. -
rtgui/paramsedited.cc—foo = v;inset().
Processing:
-
rtengine/improcfun.h— declarevoid foo(Imagefloat *rgb);. -
rtengine/ipfoo.cc— implement (guard →setMode→ read params → pixel loop). -
rtengine/improcfun.cc— insertSTEP_(foo);in the rightStage; optionally bumpNUM_PIPELINE_STEPS.
GUI + events:
-
rtgui/foo.h/rtgui/foo.cc— the panel (events viaProcEventMapper). -
rtgui/toolpanelcoord.h—#include "foo.h"+Foo *foo;. -
rtgui/toolpanelcoord.cc— allocate +addfavoritePanel(<tab>, foo);. -
rtgui/partialpastedlg.cc— one row inget_toggles.
Strings + build:
-
rtdata/languages/default—TP_*,HISTORY_MSG_*,PARTIALPASTE_*keys. -
rtgui/CMakeLists.txt—foo.cc. -
rtengine/CMakeLists.txt—ipfoo.cc.
Reference tools to copy from
| Aspect | Read this |
|---|---|
| Minimal params + panel | CACorrParams in procparams.*, rtgui/cacorrection.* |
| Params + panel + enum combo + dynamic events | Film Grain: rtgui/grain.*, GrainParams |
| Minimal processing method | rtengine/ipsoftlight.cc |
| Processing that delegates to another tool | rtengine/ipgrain.cc |
| Lab-space, mask-aware, stop-returning tool | rtengine/iplocalcontrast.cc |
| Mask / region tool (params) | SmoothingParams, ColorCorrectionParams |
Uh oh!
There was an error while loading. Please reload this page.
Hey there,
I wanted to ask what your thoughts are about two module ideas.
The first module is about placing watermarks on top of an image. These could be read from simple text, SVGs or PNGs.
The second module is about framing an image with a border filled with e.g. a solid color.
I don't know, if these ideas were up for discussion yet and just disregarded, however these are the only two things I do to my pictures outside of ART. If you like the ideas and are considering implementing then, feel free to reach out. I'd love to take over the responsibility for implementing one or both of them to give back to the project.
Cheers!
All reactions