Skip to content

Latest commit

 

History

25 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

This project provides a robust C++ toolkit for image file I/O, designed to integrate seamlessly with the acrion/image container library. It bundles the powerful ImageMagick library for handling a wide range of common image formats and NASA's cfitsio library for native support of the FITS astronomical data format.

Highlights

  • Modern C++ library for reading/writing images.
  • ImageMagick backend for common formats (JPEG, PNG, TIFF, BMP, TGA, etc.).
  • FITS via cfitsio: uses the reference implementation, which is vendored (included directly) in this repository.
  • Container-first design: interoperates with the separate acrion/image container library.
  • Plugin mode: can be loaded by nexuslua to process images asynchronously via message passing.
  • Zero-drama Windows runtime: at build time, the official ImageMagick build recipe (from the MSYS2 MINGW packages) is fetched, modified to support 32-bit depth, built, and then copied next to your binaries, including all modules and .la files; no global installation is required.

Used by acrionphoto: This plugin provides the default image I/O for acrionphoto.
acrionphoto will run without it, but cannot open or save images until an I/O plugin is installed.

The I/O role is replaceable: any plugin can provide image I/O as long as its main.lua declares the expected messages and parameters (see loadpath/savepath conventions below). This keeps I/O modular and allows alternative or additional providers.

Drawing

Five messages draw into the working image. They are implemented in Lua, in main.lua, not in C++ - deliberately, because this plugin is also meant to be read as an example of what a plugin can do without leaving Lua. The pixel work uses peek, poke and addoffset, which is the whole of the nexuslua memory API.

Where the boundary is: the messages that touch every pixel of an image (invert, the subtractions, the copies) are C++, because a full pass over a 3314x2489 frame is eight million round trips through the Lua stack. A shape touches its own outline or its own area, which is orders of magnitude less. If you need a filled shape the size of a whole frame, that is the moment to move that one function to C++ - not the rest of the file.

message draws
CallDrawLine a straight line from x0,y0 to x1,y1, lineWidth pixels thick
CallDrawRectangle a rectangle at rectX,rectY of rectWidth x rectHeight, outlined or filled
CallDrawEllipse an ellipse around centerX,centerY with radiusX,radiusY, outlined or filled
CallFloodFill fills the region around seedX,seedY whose brightness is within tolerance
CallDrawText text at textX,textY in a built-in 5x7 font, magnified scale times

The rectangle does not call its geometry width/height: those name the geometry of the image in every message of every image plugin.

These five take their coordinates as parameters, so they are what a script or another plugin calls. What a user reaches for is a tool - see below.

Text

CallDrawText needs no font from the system and no help from the host: the glyphs are part of main.lua. A plugin has to work without acrionphoto, so asking the host to render a string is not an option, and depending on an installed font would make the same call produce different pixels on different machines.

The font covers the 95 printable ASCII characters. \n starts a new line at the original textX; a byte outside that range - the two bytes of a UTF-8 umlaut, for instance - is drawn as a filled block rather than skipped, so that a caller sees which plugin cannot render it instead of wondering where the character went.

scale magnifies each font pixel into a square block, so text stays sharp at any size and a caller can predict exactly how much room it takes. The reply carries textWidth and textHeight, which is the one thing about a string a caller cannot work out from its own arguments - and what centring a label or drawing a box around it both need.

The glyphs are stored in main.lua as a picture of themselves: sixteen side by side per band, five columns each, seven rows tall. The usual form is 475 hexadecimal numbers, and a typo in that is invisible in review, invisible in a diff, and beyond what a test can catch - "something was drawn" passes whatever the shape turns out to be. Written as a sheet, reading the font is proof-reading it. The parser checks the width of every row, so a dropped character is an error when the plugin loads rather than a subtly wrong letter later.

Colour

A colour is three values in [0, 1] - colorRed, colorGreen, colorBlue, each defaulting to 1.0 - mapped onto the displayed brightness range of the image (minBrightness .. maxBrightness). The same call is therefore correct on an 8-bit photograph and on a floating point FITS frame, and a plugin never needs to know the depth of the image it draws on.

For an ordinary integer image that range is the full range of the type, so 0.5 is mid gray. For a FITS frame it is the range of the data, which is the only meaningful notion of "white" a floating point image has. On a grayscale image the luma of the colour is used, so drawing "red" and drawing "green" differ as they should.

Alpha is never written. Drawing a shape on a four-channel image changes its colour, not its transparency.

Clipping

clipX, clipY, clipWidth, clipHeight restrict a call to a rectangle; a width or height of zero means the whole image. It is a parameter of every call, never a mode you set beforehand - an agent may be replicated, and each replica runs the script in its own Lua state, so a "current clip rectangle" would work until someone passed threads and would then silently stop applying to part of the drawing.

Coordinates outside the image are clipped rather than rejected, so a rectangle half off the canvas draws its visible half instead of nothing.

What comes back

Every drawing message replies with the region it changed:

{ invalidate = { x = 4, y = 4, width = 9, height = 9 } }

A host can repaint that rectangle instead of the whole image, which on a large frame is the difference between an interactive plugin and an unusable one. The field is absent when nothing changed at all - which is a different statement from a rectangle of size zero.

What the field promises, and what a plugin sending it takes on: everything outside the rectangle is unchanged, in every image the message received - not only in the working one. acrionphoto refreshes both panes from it, so a message that changed the reference image elsewhere and still reported a rectangle would leave the display showing the old pixels. A plugin that cannot make that promise omits the field and gets a full repaint, which is what every plugin written before this contract existed gets.

The four values must be integers. A plugin computing its bounds with a division sends floating point numbers, and the receiving side treats that as no promise at all: the image would simply stop refreshing where it should. Use // rather than /.

CallDrawText additionally returns textWidth and textHeight, the size of what it drew.

CallFloodFill additionally returns filled, the number of pixels it changed. That is what tells "the seed already had that colour" from "the whole image was one region" - the two outcomes a wrong tolerance produces, indistinguishable from the outside otherwise.

Tools: drawing with the mouse

A message that declares x and y becomes a checkable button in acrionphoto's tool bar. The user selects it and then draws in the image: the host injects the pixel coordinate under the mouse, on the press and on every position of the drag. That is how one paints, and it needs nothing else - no mode to switch on, no settings stored between messages.

tool does
DrawWithBrush paints a disc of radius in the chosen colour wherever the mouse goes
DrawWhitePixel the minimal example: one white pixel, written in Lua with poke
GetPixelValueOfChannel the other minimal example: reports the values under the cursor

⚠️ A tool is asked for its parameters once per stroke, at the press, and every position of the drag reuses those answers - otherwise a dialog would stand between the user and each pixel. So everything adjustable has to be a declared parameter with a default, and nothing else should be declared: whatever is declared is what the user is asked for before the stroke begins. DrawWithBrush declares radius and the three colour components, and that is its whole dialog.

Reacting to the mouse without a tool

A message that declares requestUserInput is not invoked by the user at all: acrionphoto sends it the state of the mouse on every move, press, release and wheel notch, of both panes, whichever tool is selected and whether the plugin is interested or not.

⚠️ This is not how to make a drawing tool - the section above is. The broadcast exists for what a tool cannot express: an interactive overlay that has to follow the mouse without the user having selected anything, which is what an immediate-mode widget library will need. OnUserInput in main.lua demonstrates the contract and draws nothing.

The parameters are the seven image fields plus isRightImage, mouseX, mouseY, mouseLeftButtonPressed, mouseRightButtonPressed, mouseMiddleButtonPressed, mouseWheelUp, mouseWheelDown and, on the last broadcast a viewer ever sends, stop. Three of them are not guaranteed and a handler has to say so:

  • mouseX/mouseY are absent until the mouse has moved once;
  • the stop broadcast carries no image at all - the buffer is about to go away;
  • a broadcast arrives for the reference pane too.

Return nothing when you did not act. A handler that returns a table is answered, and an answer per mouse move is pure cost; a handler that did draw returns invalidate and that is what makes the host repaint. Test the cheapest condition first, and use metadata.pending to skip positions that a newer one has already made obsolete - see addmessage.

Using the plugin without acrionphoto

The plugin works from the nexuslua interpreter; acrionphoto is one caller of it, not a prerequisite. examples/draw.lua opens an image, draws on it and saves it:

nexuslua examples/draw.lua input.png output.png

Buffer lifetime. An image buffer stays alive as long as something holds a reference to it: the message that carries it, and any reply that carries it on. A Lua variable does not, because a pointer is a string in Lua and a string holds nothing - so the fields have to be passed along from one message to the next rather than stashed in a global.

Until BUG-34 was fixed this was worse than a rule of thumb: every message an agent handled decremented the reference counts of buffers that earlier messages had allocated, so a script that drew on an image and then saved it failed three times in five. If you are running an older build and see acrion::image::Bitmap constructor from a message that should have worked, that is what it was.

Supported formats & notes

  • Read: TIFF (.tif/.tiff), FITS (.fit/.fits), PNG (.png), JPEG (.jpg/.jpeg), BMP (.bmp), TARGA (.tga), DICOM (.dcm) — subject to ImageMagick delegates present at runtime.
  • Write: TIFF, FITS, PNG, JPEG, BMP, TARGA.
  • Bit depths (read/write via ImageMagick): 8/16/32/64-bit unsigned per channel.
  • Floating-point images are supported via FITS (read/write); colored FITS output is currently not supported.
  • Format support ultimately depends on which ImageMagick coders are available in your build (this repo takes care of shipping them on Windows).

How ImageMagick is integrated

  • The project builds or stages the official ImageMagick packages per platform and exposes them via IMPORTED CMake targets.

  • On Windows (MSYS2 / UCRT64):

    • We build from mingw-w64-imagemagick (with a modified configuration, primarily changing the quantum depth to 32-bit) and copy the runtime DLLs, modules, and .la files into your build and plugin directories.

    • Modules are flattened to:

      <your-binary-dir>/
        modules-Q32/{coders,filters}/...
        config-Q32/...
      

      (No versioned ImageMagick-7.x.y parent directory.)

    • At runtime, the library sets:

      • MAGICK_CODER_MODULE_PATHmodules-Q32/coders
      • MAGICK_FILTER_MODULE_PATHmodules-Q32/filters You don’t have to set these yourself.
  • On Linux:

    • The necessary ImageMagick shared libraries are copied directly into the build's output directory. The executable's RPATH is then patched to ensure it loads these local libraries instead of any system-wide installations. All required ImageMagick modules are linked directly into the main shared library, which simplifies runtime configuration by eliminating the need to set module search paths.
  • On macOS

    • macOS support is currently under development. The planned approach is to adapt the official Homebrew formula for ImageMagick to meet this project's specific requirements (e.g., Q32 quantum depth). This is achieved by creating a custom local formula file and instructing Homebrew to build ImageMagick from source with the necessary modifications, analogous to the process on other platforms.

On all platforms, the library programmatically sets MAGICK_HOME and other necessary environment variables at runtime. This configuration is handled internally and is confined to the process using the library, ensuring that it does not interfere with the global user environment or other applications.

Building

Prerequisites

  • CMake ≥ 3.25

  • A modern C++ compiler

  • Ninja (recommended)

  • Platform tooling:

    • Windows: MSYS2 UCRT64 shell (ucrt64.exe)
    • Linux (Arch-based): pacman (for the ImageMagick build path used here)
    • macOS: Homebrew (for toolchain/Qt if needed)

This library is also built automatically by the umbrella repo nexuslua-build. If you plan to build the full stack (nexuslua, plugins, optional GUI), start there.

Configure & build (standalone)

# From repo root
cmake -B build -D CMAKE_BUILD_TYPE=Release acrion_image_tools/
cmake --build build --parallel

Artifacts are written to cmake-build-*/bin and the project root (for the plugin build results). On Windows, you’ll see libMagick* DLLs and the modules-Q32/config-Q32 folders placed automatically next to the binaries.

Tests

A minimal GoogleTest executable (acrion_image_tools_test) is produced. You can run it directly from cmake-build-*/bin.

Using the C++ API

#include "acrion_image_tools/io.hpp"
#include "acrion/image/bitmap.hpp"

using acrion::imagetools::io;

int main() {
  std::string warning;
  auto bmp = io::Read(L"input.tif", warning);   // Returns shared_ptr<acrion::image::Bitmap>
  if (!warning.empty()) {
    // handle non-fatal warnings from ImageMagick
  }

  // ... process bmp ...

  io::Write(*bmp, L"output.png", warning);
  return 0;
}

Version helpers

#include "acrion_image_tools/version_acrion_image_tools.hpp"

std::string lib_version  = acrion::imagetools::GetVersion();
std::string im_version   = acrion::imagetools::GetImageMagickVersion();
std::string fits_version = acrion::imagetools::GetCfitsioVersion();

Using as a nexuslua plugin

The built repository can be installed or directly symlinked as a nexuslua plugin. It exposes high-performance C++ functions for image processing, which are defined and made available as a nexuslua agent, communicating via concurrent messages.

Available Operations

The plugin provides a range of operations accessible through nexuslua messages:

  • File I/O: CallOpenImageFile, CallSaveImageFile
  • Image Manipulation: CallSwap, CallCopyLeftToRight, CallCopyRightToLeft, CallInvertImage
  • Arithmetic: CallSubtract* (no wrap, wrap, absolute difference)
  • Pixel Operations: GetPixelValueOfChannel, DrawWhitePixel, etc.

Scripting in nexuslua

In addition to the compiled C++ functions, the plugin architecture allows for direct pixel manipulation in nexuslua via memory access functions (peek, poke), enabling rapid prototyping of custom algorithms. For more details on the nexuslua scripting API, please refer to the official nexuslua documentation.

Dev install into nexuslua

./install-development-plugin.sh

This script creates a symbolic link from the project directory to your local nexuslua plugin folder. This is a convenient way to test changes live without needing to reinstall the plugin after every build.

Plugin metadata (nexuslua_plugin.toml)

During build, a nexuslua_plugin.toml is written into the plugin root with metadata consumed by the acrionphoto Plugin Manager and by nexuslua tooling. Example:

displayName = "acrion image tools"
version = "1.0.246"
isFreeware = true
description = "A set of essential tools for basic image manipulation."
urlHelp = "https://github.com/acrion/image-tools"
urlLicense = "https://github.com/acrion/image-tools/blob/main/LICENSE"
urlDownloadLinux = "https://github.com/acrion/image-tools/releases/download/1.0.246/image-tools-Linux.zip"
urlDownloadWindows = "https://github.com/acrion/image-tools/releases/download/1.0.246/image-tools-Windows.zip"
#urlDownloadDarwin = "https://github.com/acrion/image-tools/releases/download/1.0.246/image-tools-Darwin.zip"
  • The macOS download URL is commented while the macOS build is under refactoring.
  • If isFreeware = false and urlPurchase is provided, acrionphoto shows “Get License Key…”; it also exposes “Install key or other files…” for copying data into the plugin’s persistent/ folder.
  • The planned acrion/nexuslua-plugins repository will hold only the URLs of such TOML files; the files themselves live with each plugin.

Project structure (selected)

main.lua                      # nexuslua message definitions and C++/Lua bridge
acrion_image_tools/
  nexuslua_plugin.toml.template # Template for nexuslua plugin metadata file
  CMakeLists.txt                # Main build logic, including ExternalProject for ImageMagick
  io.hpp|cpp                    # Public API for image I/O via ImageMagick + FITS
  fits.hpp|cpp                  # FITS reading/writing using the vendored cfitsio library
  imagemagick.hpp               # ImageMagick headers/config (Q32 depth, HDRI toggle)
  main.cpp                      # C++ entry points for functions exposed to nexuslua
  im/                           # ImageMagick build glue (PKGBUILDs, patches, scripts)
  cfitsio/                      # vendored cfitsio reference implementation
  version_*                     # Version query utilities (library, IM, cfitsio)

Troubleshooting

  • “NoDecodeDelegateForThisImageFormat 'JPEG'” On Windows, ensure the runtime directory contains modules-Q32/coders/jpeg.dll and jpeg.la next to your application binary. Set the environment variable MAGICK_DEBUG=Module,Coder to see verbose module lookup paths.
  • Running from a different working directory The library derives necessary paths from the current binary's location, not the current working directory (CWD). As long as the dependent libraries and modules are placed next to your executable, they will be found.

Community

Have questions about the library, want to share what you've built, or discuss image processing techniques? Join our community on Discord!

💬 Join the acrion image Discord Server


Licensing

acrion image-tools is dual-licensed to support both open-source development and commercial use.

  1. AGPL v3 or later: For use in open-source projects that are compatible with the AGPL.
  2. 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. For commercial licensing inquiries, please contact us at https://acrion.ch/sales.

Third-Party Libraries

acrion image-tools incorporates or links against several third-party libraries. Their licenses apply to their respective components and must be respected.

  • cfitsio: The source code is included directly in this repository. It is released under a permissive, public-domain-like license granted by NASA. The full license text is available in acrion_image_tools/cfitsio/License.txt.
  • ImageMagick: This library is downloaded and built at compile time. It is distributed under the ImageMagick License, which is a permissive, Apache 2.0-style license. You can find more details on the official ImageMagick website.

About

A C++ library for image I/O using ImageMagick and cfitsio, designed as a nexuslua plugin for asynchronous image processing.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages