acrionphoto is a Qt desktop application that pairs a clean two-pane workflow with a powerful, asynchronous plugin system built on nexuslua. The app itself stays small and focused; the heavy lifting (I/O, processing, interactive tools, custom dialogs) lives in plugins. This architecture keeps the UI responsive while you watch results stream in.
After a long period of private development, acrionphoto is now released to the community. The plugin API is open - anyone can build plugins that integrate as if they were shipped with the app.
Development Status
- The core app runs stably.
- Installers for macOS, Windows, and Linux are not yet available.
- Plugin registry integration (with acrion/nexuslua-plugins) is planned but not yet implemented.
- The macOS build is currently incomplete due to recent refactoring.
- The
image-tools
plugin build currently relies onpacman
; see the Build section for platform notes.
The original motivation was to create a multi-platform successor to my older commercial astrophotography tool, Straton. Early work on acrionphoto dates to ~2015, and the project gradually evolved into a general-purpose tool, especially after the plugin concept was introduced. An astrophotography plugin for acrionphoto already exists and will be released soon (likely as open source).
nexuslua was designed to solve a common problem in IDEs: plugins that block the UI or behave unpredictably. It uses a concurrent messaging system to ensure responsiveness. The Lua-free parts of the messaging core live in the C++ library Cbeam.
acrionphoto's concept is similar to ImageJ but features a simpler, Lua-based plugin interface and built-in asynchrony via nexuslua. Plugins can also extend the UI in several ways, including with fully custom dialogs.
- Two synchronized panes are at the core of the workflow: Reference (left) and Working (right). This fixed layout makes before/after comparisons, A/B testing, and iterative workflows feel natural.
- Plugin-configurable areas:
- The top button row (in the screenshot: Open, Save right, Color Pick, Set Pixel) is populated by plugins.
- The left navigation can host a plugin entry that exposes its main functions (see acrion image tools and acrion imago below).
- Recent Files lists thumbnails for both panes, providing quick visual context.
- The Plugin Manager lists installed plugins with their Installed Version, Online Version, and License status.
- Depending on plugin metadata (from
nexuslua_plugin.toml
), license-related buttons appear (e.g., acrion imago shows actions to install or view a license file). - Install/Uninstall are fully implemented. Next steps include:
- Integration with a public registry at
acrion/nexuslua-plugins
. - The manager shows only a subset of nexuslua plugins, filtering for those whose
main.lua
declares messages compatible with acrionphoto.
- Integration with a public registry at
Note: acrionphoto is tightly integrated with the nexuslua ecosystem. The Plugin Manager filters and displays plugins that adhere to acrionphoto's message and parameter conventions (details below).
This plugin serves as both a demo and an essential component: it provides image I/O and fundamental operations. While acrionphoto can run without it, you won't be able to open or save images.
Highlights:
- Operations can be composed. The brightened Working image above was produced by applying Invert Image followed by Difference.
- Some functions are Lua-only (e.g., right-left (wrap)) to demonstrate pure-Lua processing, while others call native libraries via
import(...)
. - The plugin exposes interactive utilities (mouse test, pixel pick/set) to illustrate how to build coordinate-driven tools.
The (unreleased) acrion imago plugin demonstrates fully custom dialogs defined declaratively in Lua. You don't write Qt UI code; acrionphoto reads your parameter descriptors and renders the dialog automatically:
- Numbers become sliders with
min
/max
ranges. - Enums become dropdowns with inline help text from
values
. - Booleans render as checkboxes or toggles.
- Mark parameters
internal=true
to hide them from the UI while keeping them available to the algorithm. - Set per-message icons, display names, and descriptions.
The Lua snippet below defines the "Visualize distortion" dialog shown in the screenshot:
local ImageParameters <const> =
{
imageBuffer = { type = "void*" },
width = { type = "long long" },
height = { type = "long long" },
channels = { type = "long long" },
depth = { type = "long long" },
}
local CommonParameters <const> = mergetables(ImageParameters,
{
InterpolationModel = { type = "enum", default = "Center And Borders", values = {["Center And Borders"]="", Continuous=""}},
ConsiderEdges = { type = "boolean", default = true, values = {[false]="only consider corners, not edges (affects interpolation model 'Center And Borders')", [true]="consider the edges additional to the corners (affects interpolation model 'Center And Borders')"}},
ConsiderCenter = { type = "boolean", default = false, values = {[false]="affects interpolation model 'Center And Borders'", [true]="affects interpolation model 'Center And Borders'"}},
ConsiderTopLeftCorner = { type = "boolean", default = true, values = {[false]="affects interpolation model 'Center And Borders'", [true]="affects interpolation model 'Center And Borders'"}},
ConsiderTopRightCorner = { type = "boolean", default = true, values = {[false]="affects interpolation model 'Center And Borders'", [true]="affects interpolation model 'Center And Borders'"}},
ConsiderBottomLeftCorner = { type = "boolean", default = true, values = {[false]="affects interpolation model 'Center And Borders'", [true]="affects interpolation model 'Center And Borders'"}},
ConsiderBottomRightCorner = { type = "boolean", default = true, values = {[false]="affects interpolation model 'Center And Borders'", [true]="affects interpolation model 'Center And Borders'"}}
})
addmessage("CallVisualize", {
displayname="Visualize distortion",
description="Analyze distortions in the left image and visualize them",
icon="Visualize.svg",
parameters=mergetables(CommonParameters, {
referenceImageBuffer = { type = "void*" },
StepSize = { type = "long long", default = 20, minimum = 0, maximum = 20 }
})
})
referenceImageBuffer
(the left image data) is automatically supplied by acrionphoto. For the right image, use imageBuffer
.
Commercial plugins can report their license state via a simple Lua function:
function IsLicensed()
import("acrionimago", "CheckLicense", "bool()")
import("acrionimago", "GetLicensee", "const char*()")
return CheckLicense(), GetLicensee()
end
The same metadata drives both the left-hand tool panel and the runtime dialogs, keeping the UI and execution declarative and in sync.
acrion imago may be published as open source later or kept closed source. The Lua metadata is shared here to document the dialog-building mechanism.
- Overlay Left (press and hold): Toggles the Reference image overlay on the Working pane for quick "blink" comparisons.
- Overlay Diff (press and hold): Shows the pixel-wise difference between the two panes.
- Swap Images: Swaps the reference and working images.
- Zoom: Full (fit to view) and 1:1 (pixel-perfect).
- Gamma: Brightens the view only without altering pixel data - invaluable for inspecting low-signal images, such as in astrophotography.
This is not a gimmick - it unlocks a new paradigm for building visual tools.
acrionphoto can forward mouse events from the image panes to plugins. In response, plugins can draw back asynchronously into the Working image. This simple loop is all you need to implement a complete interactive application inside acrionphoto's panes:
- Create brush tools, spot repair tools, selection markers, measurement aids, ROI widgets, etc.
- Render complex UIs directly into the image. A plugin could even use a third-party immediate-mode GUI (e.g., cvui) to draw controls.
- The host application remains responsive because all plugin communication runs through nexuslua's asynchronous message queues.
How it works:
- If your message's parameter list includes both
x
andy
, acrionphoto treats it as coordinate-driven. - On mouse events, the host sends a message with
{ imageBuffer, width, height, ..., referenceImageBuffer, x, y, ... }
. - Your plugin processes the event, updates the image buffer(s), and returns a result table. acrionphoto then refreshes the pane.
- You can chain this with inter-plugin calls for modular, powerful toolsets.
A demo plugin showcasing this capability is planned. Contributions are welcome!
acrionphoto is a message-driven shell for image manipulation. All meaningful work happens in plugins hosted by nexuslua. A plugin is typically a folder containing a main.lua
file that:
- Declares messages via
addmessage(name, { displayname, description, icon, parameters = {...} })
. - Describes parameters with rich metadata:
- Types:
string
,boolean
,double
,long long
,enum
,void*
(for image buffers), and special I/O typesloadpath
andsavepath
. - Metadata:
default
,min
/max
ranges, andvalues
(for enum documentation). internal=true
hides a parameter from the UI while keeping it available to the algorithm.
- Types:
- Calls native code with
import("libname", "Function", "signature")
. - Sends messages to other plugins with
send("Plugin Name", "MessageName", parameters)
. - Returns results as a Lua table (often including a new
imageBuffer
, brightness bounds, and status/error messages).
Under the hood:
nexuslua::agents
hosts each plugin in its own threaded agent (Lua or C++).nexuslua::AgentMessage
describes messages, their parameters, and icons.nexuslua::LuaTable
(a typed, nested map) carries parameters and results.- [Cbeam] provides logging, cross-library memory management, and serialization.
Since nexuslua
plugins are general-purpose, acrionphoto
relies on conventions to identify and integrate relevant plugins. For a plugin to be recognized, it must adhere to specific message and parameter patterns.
The core convention revolves around passing image data. Plugins should define and use a standard set of parameters for images:
local ImageParameters <const> =
{
imageBuffer = { type = "void*"},
width = { type = "long long"},
height = { type = "long long"},
channels = { type = "long long"},
depth = { type = "long long"},
}
-
I/O Messages (Open/Save) A message is treated as an Open action if it declares a parameter named
path
of typeloadpath
. It's treated as a Save action if thepath
parameter has typesavepath
. An optionalfilter
parameter (string) can define the file dialog filter (e.g.,"*.fits;*.tif;*.png"
).Internally (
src/PluginManager.cpp
):areIOparams(...)
detects thepath
(loadpath
/savepath
) and optionalfilter
.IsInputMessage(...)
andIsOutputMessage(...)
classify messages based on this.RunIoPluginMessage(...)
injects the user-selected path and sends the message.
-
Image Plumbing Before sending a message, acrionphoto injects image context using
PluginManager::AddImageParameters(left, right, parameters, coordinate)
:- The Working image's data (
imageBuffer
,width
,height
, etc.) becomes the default parameter table. - The Reference image buffer is exposed as
referenceImageBuffer
. - Mouse coordinates are added as
x
andy
for interactive tools. - Your message's parameters are merged last (with collision checks).
- The Working image's data (
-
Requesting Ad-Hoc Input If a message includes the
requestUserInput
parameter, the UI can broadcast user input back to it. This is ideal for interactive, multi-step, or long-running operations.
Plugins can call each other, allowing you to compose functionality and keep code modular. For example, your main.lua
can define a message that internally calls another plugin:
addmessage("CallTestMessageInvert", {
displayname="Test message",
description="Sends an 'invert' message to the image tools plugin",
parameters=mergetables(ImageParameters,
{
minBrightness = { type = "double"},
maxBrightness = { type = "double"}
})
})
This code adds a button to your plugin's UI that, when clicked, uses the acrion image tools
plugin to invert the current image.
- Each plugin runs in a separate agent thread.
- The UI posts messages and returns immediately, preventing any blocking.
- Image buffers use Cbeam's stable reference buffer:
- Cross-library reference counting keeps pointers valid, even across different shared libraries (
.dll
,.so
,.dylib
). - A scoped
delay_deallocation
mechanism allows raw pointers to be used safely during Lua ↔ C++ calls.
- Cross-library reference counting keeps pointers valid, even across different shared libraries (
- Results stream back as Lua tables. The Working pane updates automatically, so you can watch processing happen in real time while the UI remains fully interactive.
acrionphoto itself is agnostic to pixel formats. All image data is managed by the shared library acrion image
:
- A single container holds the
imageBuffer
,width
,height
,channels
,depth
, and view hints (minBrightness
,maxBrightness
,gamma
). - Supported per-channel bit depths include 8/16/32/64-bit unsigned integers and 64-bit floating point.
- Plugins read from and write to the same container structure. This is how
acrion image tools
can provide I/O for FITS, TIFF, PNG, etc., without the core application needing to know about file formats.
Please refer to https://github.com/acrion/nexuslua-build to build acrionphoto and its dependencies.
- The image-tools plugin currently requires pacman for its dependencies:
- On Linux, an Arch-based distro is the most straightforward path.
- On Windows, the build uses MSYS2/ucrt64, where pacman is available.
- The macOS build of
image-tools
is being updated to align with the latest refactoring for this public release.
You can build acrionphoto
as a single repository:
# from repo root
cmake -B build src/
cmake --build build
Important:
acrionphoto
runs without plugins, but has no image I/O unless an image I/O plugin (e.g. acrion image tools) is installed. That means the UI starts and you can open About/Licences, but you cannot open/save images until a compatible plugin is present.
How to get image I/O today
-
Option 1 (easiest): build via the umbrella repo, which also builds the acrion image tools plugin:
-
https://github.com/acrion/nexuslua-build (
--profile acrionphoto
) -
Afterwards, run the development install script once to link the plugin into your local nexuslua plugin folder:
# from the plugin repo root ./install-development-plugin.sh # Example output: # Plugin name: 'acrion image tools' # Successfully created symbolic link: /home/<you>/.local/share/nexuslua/plugins/acrion image tools
-
-
Option 2 (soon): install plugins directly from the public registry (
acrion/nexuslua-plugins
). The manager already supports installation/update/uninstall; the registry hookup will deliver the metadata URLs.
There are no installers yet. Although the current build path for dependencies uses pacman, future platform-specific installers will not require it.
- Launch the built application (it will include the acrion image tools plugin if you build via nexuslua-build).
- Open an image.
- Explore the tools. For example, in the acrion image tools panel, click Invert Image, then Difference. Afterward, switch to the View Pane and click & hold "Overlay Left" or "Overlay Diff" to visualize the modifications.
A minimal main.lua
is all you need to get started.
-- 1) A simple "Open" message (identified as "Open" due to 'loadpath')
addmessage("OpenRight", {
displayname = "Open (right)",
description = "Load an image into the Working pane",
icon = "FileOpen.svg",
parameters = {
path = { type = "loadpath" }, -- This marks the message as an "Open" action
filter = { type = "string", default = "*.fits;*.tif;*.png" }
}
})
-- 2) A coordinate-driven operation (fires on clicks in the Working pane)
addmessage("SpotOp", {
displayname = "Spot operation",
description = "Process a small area around a clicked coordinate",
icon = "Start.svg",
parameters = {
-- These are filled automatically by acrionphoto:
imageBuffer = { type = "void*" },
width = { type = "long long" },
height = { type = "long long" },
channels = { type = "long long" },
depth = { type = "long long" },
referenceImageBuffer = { type = "void*" },
-- These make the message coordinate-driven:
x = { type = "long long" },
y = { type = "long long" }
}
})
Guidelines
- Use
void*
for image buffers and always includewidth
,height
,channels
, anddepth
. - Include
referenceImageBuffer
if your tool needs to read from the left image. - Add
x
andy
parameters to receive mouse coordinates from the image panes. - Use
import(...)
to call native C/C++ code from shared libraries. A common signature is"table(table)"
, which takes a parameter table and returns a result table. - To expose I/O functionality, name a parameter
path
and set its type toloadpath
(Open) orsavepath
(Save). You can also provide afilter
. - Return a Lua table with a new
imageBuffer
and (optionally)minBrightness
/maxBrightness
to update the Working pane.
src/
- The main Qt application (MainWindow, panels, dialogs, dark theme).MainWindow.*
,ImageViewer*.*
,ControlsPanel/*
,Dialogs/*
- Core UI components.PluginManager.*
- Handles plugin discovery, installation, and message dispatching (I/O detection, coordinate routing, image parameter injection).Runtime/Resources/Help/{en,de,fr}/
- Built-in help files.
screenshots/
- Images embedded in this README.LICENSE
- Dual-license terms (AGPL/Commercial).
acrionphoto, like nexuslua and Cbeam, is intended to be a community-driven project. Join the conversation on our Discord server, share ideas, or get help.
💬 Join the acrionphoto Discord Server
Here are some ways you can contribute:
- Share ideas and open issues.
- Contribute plugins (for processing, I/O, educational purposes, or interactive tools).
- Help shape the Plugin Marketplace and its metadata standards.
- Improve the documentation and add examples.
If you have built interesting Lua pipelines or C++/CUDA/OpenCL filters, we would love to see them wrapped as plugins!
acrionphoto is dual-licensed to support both open-source development and commercial use.
- AGPL v3 or later: For use in open-source projects that are compatible with the AGPL.
- Commercial License: For integration into proprietary applications or for cases where AGPLv3 terms cannot be met.
I would like to emphasize that offering a dual license does not restrict users of the normal open-source license (including commercial users). The dual licensing model is designed to support both open-source collaboration and commercial integration needs.