This is a series of posts on building imgplex, best read in order:
Part 1 - The why, what, and how of imgplex
Part 2 - Getting things up and running
Part 3 - The node definition system
Part 4 - Executing the node graph, making it fast
Part 5 - Two graphs in one
Part 6 - Multiple inputs and outputs, processing images as sets
Part 7 - Profiling and Optimization
The node definition system
Once the test node was working, I needed a way to add the rest of the ImageMagick operations. I could implement each node as a TypeScript component, but that would tie every addition or parameter change to the application code. I also wanted to be able to adjust labels, parameter ranges, and command templates without rebuilding the application. I moved that information into JSON files loaded when the application starts.
It also left open the possibility for users to add straightforward ImageMagick operations themselves, as long as the node could be expressed through the existing definition format. In development, the registry even hot-reloads when a file changes, so you can iterate on a node definition and see the result in the running app immediately.
The setup is similar to using ScriptableObjects for content definitions in Unity: the application provides the behavior, while each JSON file describes a particular node.
What a node definition looks like
Here’s a simplified example - the Brightness/Contrast node:
{
"id": "brightness_contrast",
"version": "1.0.0",
"label": "Brightness / Contrast",
"description": "Adjust image brightness and contrast",
"aliases": ["exposure", "levels adjustment"],
"category": "Color",
"inputs": [
{
"type": "image",
"label": "Input"
}
],
"outputs": [
{
"type": "image",
"label": "Output"
}
],
"params": [
{
"name": "brightness",
"label": "Brightness",
"type": "int",
"widget": "slider",
"default": 0,
"min": -100,
"max": 100
},
{
"name": "contrast",
"label": "Contrast",
"type": "int",
"widget": "slider",
"default": 0,
"min": -100,
"max": 100
}
],
"command_template": "-brightness-contrast {{brightness}}x{{contrast}}"
}
The command_template field is an ImageMagick command with parameter placeholders in
curly braces. At execution time the pipeline substitutes the actual parameter values
and passes the result to magick. For most processing nodes, this template is the
only node-specific information the execution system needs.
The params array is used to build the controls in the inspector. The same
definitions also create input ports on the node, allowing a parameter to receive its
value from elsewhere in the graph.
When a workflow is exported as a script, the final resolved values are inserted into the same command template. This kept the editor and command-line export using the same parameter definitions instead of maintaining two separate implementations.
Connecting values to parameters
Each parameter with a writable value exposes an input handle on the left side of the node. That handle can accept a wire from any compatible output port elsewhere in the graph.
This is how pure-value nodes integrate with image processing nodes. For example, the brightness parameter can use its local slider value or receive a value from a Float or Math node elsewhere in the graph. Before processing an image, the pipeline resolves that value chain and inserts the result into the ImageMagick command.
Connections are checked by type, so the editor will reject a color output connected to a float input. Each value type also uses a different wire color to make larger graphs easier to follow. I tested those colors against the dark graph background and adjusted colors that were difficult to distinguish.
The node registry
When imgplex starts, the main process scans node-definitions/ and validates each
JSON file before adding it to the registry. The renderer retrieves that registry over
IPC and uses it to build the node library and create nodes in the graph.
During development, the registry watches the definitions folder for changes. I could edit a parameter range or command template, save the file, and inspect the updated node without restarting the application.
I later kept the same behavior in packaged builds so that newly added definitions appear while the application is running.
The loader rejects definitions with missing fields, unknown executor names, or invalid parameter types. It logs the filename and validation problem and leaves that node out of the registry.
Growing the node library
Once the shared node UI and execution path were in place, I could add most of the Transform, Colors, and Filters operations by writing definitions rather than new components.
The definition format does not cover every case. Channel Split creates several image outputs, and Properties has to inspect the current file, so nodes like those use custom TypeScript executors. Ordinary operations that map directly to an ImageMagick command remain data-only.
At the time of writing, imgplex has more than 60 nodes across 12 categories, with most of them defined entirely in JSON. I later applied the same approach to output formats, which are loaded from the format-definitions/ folder.
Next post in this series: Building imgplex: part 4 - Executing node graph, making it fast