Overview
The library reads codes of several symbologies in a frame with a single call: DataMatrix (ECC200) — Chestny ZNAK marking codes (the Russian national product marking system), GS1-128 — SSCC on logistics labels, Code 128, and retail barcodes EAN-13, EAN-8, UPC-A and UPC-E. Accuracy and read speed are shown at the top of the page; below is the interface only.
The library is static; the public interface is C++20, eight headers in include/unir/, plus a flat C interface for other languages (unir_c.h, see C ABI). A frame is passed as a view over a foreign buffer (unir::ImageView) — no copying, straight from the camera buffer or image you already have.
Installation
The library ships prebuilt — you do not build it, you only build your own program. There are two ways to use it: statically — the C++ API from libunir.a, together with both engines, is linked into your program in full, or dynamically — through the unir_c library with a flat C interface (C ABI). Packages are available for Windows x64 and Linux (x86-64, ARM64 and ARMv7), with the same contents:
include\unir\{Unir,Geometry,Image,License,Scanner,Settings,Symbology,Version}.h C++ API
include\unir\unir_c.h C ABI
lib\libunir.a C++ static library (with the dmr and ssccr engines)
lib\pkgconfig\unir.pc
bin\unir_c.dll C ABI library
lib\unir_c.dll.a its import library (MinGW)
lib\pkgconfig\unir_c.pc
bin\unir.exe command-line utility
share\doc\unir\licenses\ third-party licensesIn “Downloads” in your account there are two archives per platform: unir-1.0.1-<platform>.zip (Windows) or .tar.gz (Linux) — the full package with libraries and headers, and unir-1.0.1-<platform>-cli with the same extension — the unir utility only, for checks without programming. Platforms: windows-x64, linux-x64, linux-arm64 and linux-armhf — the latter is ARMv7, a 32-bit OS (for example, a Raspberry Pi running a 32-bit system). An archive unpacks into a directory of the same name, for example unir-1.0.1-linux-x64/.
| Your program | What to use |
|---|---|
| C++, built with MinGW-w64 GCC (Windows) or GCC (Linux) | Static linking — C++ API, libunir.a |
| MSVC or another compiler; another language (C, C#, Delphi, Go…); updating the library without rebuilding the program | Dynamic linking — C ABI, unir_c |
| Python | pip install libscanner-unir or the wheel from “Downloads” — Python documentation |
Static linking — C++ API
libunir.a is linked into your program in full: nothing needs to sit next to it except the key file unir.key, which goes next to the program (see “License”). A C++20 compiler is required. The API is C++ and returns std::string and std::vector, so the compiler and its standard library must be the same as the package's (the same or a newer version).
The Windows package is built with MinGW-w64 GCC — libunir.a is in its format and with its ABI. MSVC cannot link this static archive; your program needs the same toolchain (for example, MSYS2/MinGW or CLion with a MinGW profile). From MSVC, link dynamically, via unir_c.
The toolchain is free: MSYS2 — run the installer from the website, then in its MINGW64 shell (the package is built in exactly this environment, not UCRT64):
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconfInclude paths and the link line come from unir.pc. Point an environment variable (PowerShell) at lib\pkgconfig of the unpacked package; paths in the .pc are relative to the file itself, so the unpack directory can be anywhere:
$env:PKG_CONFIG_PATH = "C:\path\to\unpacked\lib\pkgconfig"
pkg-config --static --cflags --libs unirThe system libraries winhttp, crypt32, shell32 and advapi32 (used by the license check) are already listed in the package's Libs.private — no need to add them separately.
The package is a single static library, so the caller also needs the libraries it depends on. Without --static they will not be on the command line, and linking will fail with unresolved symbols.
In meson — a dependency by package name:
unir_dep = dependency('unir', required: true, static: true)
executable('myprogram', 'main.cpp', dependencies: [unir_dep],
link_args: ['-static']) # Windows: MinGW runtime linked into the programOn Windows, -static also links the compiler runtime into the program: without it, the program will need libstdc++-6.dll, libgcc_s_seh-1.dll and libwinpthread-1.dll from MSYS2 next to it. On Linux it is not needed. A complete meson.build is in “Quick start”.
Dynamic linking — C ABI
unir_c is the same library behind a flat C interface (header unir/unir_c.h): unir_* functions and opaque pointers, no C++ types. The compiler runtime is linked into it statically, so it depends neither on the caller's compiler nor on its C++ standard library — it works with MSVC, Clang and any language that can call C functions. Everything the library returns (results, images, strings) is freed by its own functions: unir_scan_result_free, unir_image_free, unir_free_string.
The rules of the interface and every function — in C ABI.
MinGW-w64 GCC — via unir_c.pc and the import library lib\unir_c.dll.a, without --static:
$env:PKG_CONFIG_PATH = "C:\path\to\unpacked\lib\pkgconfig"
pkg-config --cflags --libs unir_cThis yields -I…\include -L…\lib -lunir_c.
MSVC — the package has no .lib file; you generate it from the DLL itself. In the MSYS2 shell, extract the export definitions:
gendef unir_c.dll # package mingw-w64-x86_64-tools; writes unir_c.defthen, in the Developer Command Prompt for VS (x64), create the import library and build:
lib /def:unir_c.def /machine:x64 /out:unir_c.lib
cl /I C:\path\to\unpacked\include main.c unir_c.libAt run time unir_c.dll must be next to your .exe (or in a directory on PATH), and the key file unir.key goes next to unir_c.dll or next to the .exe (see “License”).
In meson — likewise by package name, without static:
unir_c_dep = dependency('unir_c', required: true)
executable('myprogram', 'main.c', dependencies: [unir_c_dep])Quick start
A minimal program: load an image from disk, read it, print the symbology and the text. All you need is one header:
#include <unir/Unir.h>
#include <iostream>
int main() {
unir::Scanner scanner; // frame stream, all symbologies of the build, one code
unir::LoadStatus load = unir::LoadStatus::Ok;
const unir::ScanResult r = scanner.scanFile("frame.jpg", &load); // loading and reading under one time limit
if (load != unir::LoadStatus::Ok) {
std::cerr << "could not read the file\n";
return 1;
}
if (r.ok()) {
for (const unir::Code& c : r.codes)
std::cout << unir::nameOf(c.symbology) << ": " << c.text << "\n";
} else if (r.status == unir::Status::NotFound || r.status == unir::Status::NotDecoded) {
std::cout << "no code read\n"; // NotDecoded also means "no code"
} else {
std::cout << "frame not read, status " << static_cast<int>(r.status) << "\n";
}
}The example can be built with Meson. Next to main.cpp put the unpacked package (unir-1.0.1-windows-x64 or unir-1.0.1-linux-x64) and a meson.build file:
project('test_unir', 'cpp',
version : '0.1.0',
default_options : ['cpp_std=c++20', 'warning_level=3', 'buildtype=release'])
cxx = meson.get_compiler('cpp')
# The prebuilt unir sits next to the project; the dependency is described by
# hand, so pkg-config is not needed at all.
unir_root = meson.current_source_dir() / 'unir-1.0.1-windows-x64'
unir_lib = cxx.find_library('unir', dirs : unir_root / 'lib', static : true)
# Libs.private from unir.pc — required for static linking
unir_private = []
foreach l : ['winhttp', 'crypt32', 'shell32', 'advapi32']
unir_private += cxx.find_library(l)
endforeach
unir_dep = declare_dependency(
include_directories : include_directories('unir-1.0.1-windows-x64/include'),
dependencies : [unir_lib] + unir_private)
executable('test_unir', 'main.cpp',
dependencies : unir_dep,
cpp_args : ['-finput-charset=UTF-8', '-fexec-charset=UTF-8'],
link_args : ['-static'])Build in the MSYS2 MINGW64 shell:
meson setup build
meson compile -C buildThe same with pkg-config: the package's .pc files are relocatable, so instead of the manual description one line in meson.build is enough — unir_dep = dependency('unir', static : true) — plus the path to the unpacked lib/pkgconfig at configure time:
meson setup build --pkg-config-path="$PWD/unir-1.0.1-windows-x64/lib/pkgconfig"
meson compile -C buildFor a frame from your own capture pipeline (camera SDK, OpenCV buffer, Windows bitmap) the disk is not involved — you build a view over the buffer you already have, without copying:
const unir::ImageView view{ m.data, m.cols, m.rows, (int)m.step, unir::PixelFormat::Bgr8 };
const unir::ScanResult r = scanner.scan(view);Headers
There are seven public C++ headers, all under include/unir/; the eighth, unir/Unir.h, includes them all at once. A ninth file, unir/unir_c.h, is a separate C interface (C ABI). The headers of the dmr and ssccr engines are not shipped: their types never appear in the interface.
| Header | Contents |
|---|---|
unir/Unir.h | Umbrella header — includes everything below |
unir/Image.h | ImageView, Image, PixelFormat, ImageLimits, loadImage |
unir/Scanner.h | Scanner, ScanResult, Code, Status |
unir/Settings.h | Scanner settings and presets |
unir/Symbology.h | Symbology, the Symbologies mask, names and AIM prefixes |
unir/Geometry.h | Point, Box, Quad — where the code is located in the frame |
unir/License.h | License check and activation, the unir::license namespace |
unir/Version.h | Version of the header (macros), of the built library and of the engines inside it |
The headers pull in nothing extra: code that only builds a frame view needs just Image.h, and Symbology.h needs only <cstdint> and <string>.
Scanner
Scanner is an object, not a function, and there is no need to create one per frame. The constructor assembles the chain of engines for the requested symbologies and also performs the first license check — if a not-yet-activated unir.key lies nearby, the constructor waits for the server once, so the first frame does not pay that delay. In stream mode (Settings::stream()) the DataMatrix engine accumulates a symbol size hint across frames, and the hint has to outlive each frame.
class Scanner {
public:
explicit Scanner(Settings s = Settings::stream());
~Scanner();
Scanner(Scanner&&) noexcept;
Scanner& operator=(Scanner&&) noexcept;
Scanner(const Scanner&) = delete;
Scanner& operator=(const Scanner&) = delete;
// Read a frame. Not thread-safe on a single Scanner in Stream mode
// (the dmr hint changes from frame to frame); for several threads,
// use one Scanner per thread.
ScanResult scan(ImageView frame) const;
// An image from disk and its reading under ONE frame time limit
// (loading is included). load — what happened when reading the file
// (may be nullptr).
ScanResult scanFile(const std::string& path, LoadStatus* load = nullptr) const;
const Settings& settings() const;
};
// One-off read without an object: a probe, a script, a single file.
ScanResult scan(ImageView frame, const Settings& s = Settings::single());scan() is declared const, but in Stream mode it updates the DataMatrix size hint from frame to frame. If frames come from several threads (several cameras, a worker pool), create a separate Scanner for each thread rather than sharing one.
scanFile() reads an image from disk and the codes in it under a common time limit: loading counts against the same budgetMs / timeoutMs as reading, so a heavy file does not get a second budget. The image is loaded as grayscale (LoadAs::Gray) with the limits from Settings::imageLimits. If the file fails to load, *load receives the reason (see “Frame”), and the result status is ImageTooLarge for an oversized image and InvalidImage for every other failure; if the time limit cut decoding short, timedOut is set as well. Without a license the file is not even opened: *load stays Ok, the status is Unlicensed.
LoadStatus in C++ has four values, and there is no separate “ran out of time” among them: decoding cut short by the time limit is Unreadable together with timedOut = true. In the C ABI and in Python the same case comes as a separate value, TIMED_OUT; one more value there, INVALID_ARGUMENT (settings rejected by the ABI boundary), is not needed in C++ — the Settings type cannot be malformed here.
| What happened to the file | *load | status, timedOut | C ABI / Python |
|---|---|---|---|
| Read | Ok | outcome of the scan | UNIR_LOAD_OK / OK |
| No such file | NotFound | InvalidImage, false | UNIR_LOAD_NOT_FOUND / NOT_FOUND |
| Not JPEG/PNG, or damaged | Unreadable | InvalidImage, false | UNIR_LOAD_UNREADABLE / UNREADABLE |
| Decoding did not fit into the time limit | Unreadable | InvalidImage, true | UNIR_LOAD_TIMED_OUT / TIMED_OUT |
| Exceeds the limits according to the header | TooLarge | ImageTooLarge, false | UNIR_LOAD_TOO_LARGE / TOO_LARGE |
| No license — the file was not opened | Ok | Unlicensed, false | UNIR_LOAD_OK / OK |
scan() and scanFile() catch exceptions internally: an internal failure (for example, out of memory) becomes Status::Failed if no code has been read by then, and the process keeps running. loadImage() reports problems through LoadStatus.
Scanner can be moved but not copied — the copy constructor and copy assignment are explicitly deleted.
Scan result
Status
// The numbers are the same as in dmr and ssccr. The exit codes of the unir utility are the
// same numbers, except for file load failures (see "Command line").
enum class Status {
Ok = 0, // at least one code read
NotFound = 1, // no symbol found
NotDecoded = 2, // symbol found but not read
Unlicensed = 3, // no unir license, trial period over; the frame was not processed
ImageTooLarge = 4, // frame larger than Settings::imageLimits, not processed
InvalidImage = 5, // inconsistent frame description (row length, format)
Failed = 6, // processing aborted by an internal error; the process lives on
};Ok comes back if and only if codes holds at least one code — from any engine. If none of them read anything, the status is the weightiest of their answers: Failed, then InvalidImage, ImageTooLarge, NotDecoded and finally NotFound. This way a DataMatrix that was found but not read is not hidden behind a “no barcode” from the linear engine. With Status::Unlicensed the frame was not processed at all — the reason is given by unir::license::status() (see “License”).
What NotDecoded really means
NotDecoded does not mean “there definitely is a code”. It means that at least one engine in the mask found a candidate in the frame that looks like a symbol of its symbology, and could not read it. A candidate is not yet a symbol. The DataMatrix engine takes as a candidate anything that resembles a solid L finder pattern or a texture of modules — the bars of a linear code, printed text or a packaging pattern can all qualify. The linear engine flags a run of bars that partly matched the Code 128 or EAN/UPC pattern but failed acceptance. That is why status 2 is common on frames that contain no code of that symbology at all: a DataMatrix-only mask on a frame with a single GS1-128, an EAN/UPC-only mask on a frame with a single DataMatrix.
The status is computed only from the engines in the mask: a code of a symbology outside the mask is not searched for, and its bars or modules can become a candidate for another engine — hence NotDecoded rather than NotFound. A linear code that the engine did read but the mask does not admit (an SSCC when the mask holds only Code128) is discarded and does not produce Ok either.
Treat NotDecoded as “no code was read”: on a conveyor, take the next frame; for a one-off image, ask for a new shot or retry with a longer time limit. Do not use it as a sign that “the code is there but bad” (for rejecting products). If timedOut is set together with NotFound or NotDecoded, the answer is not final: the time limit cut the search short, and with more time the code might have been read.
For your own program the statuses boil down to three outcomes:
| Status | Outcome | What to do |
|---|---|---|
Ok | Code present | Use codes. With timedOut, some symbologies or codes may have gone unchecked |
NotFound | No code | Next frame. With timedOut — not final |
NotDecoded | No code (not read) | Next frame or a new shot; do not assume a code is present |
Unlicensed | Error | No license: show license::status().message; retrying the frame is pointless until activation |
ImageTooLarge | Input error | Downscale the frame or raise imageLimits |
InvalidImage | Input error | From scan() — a mistake in the frame description (your code); from scanFile() — the file was not read, the reason is in *load |
Failed | Error | Internal failure (for example, out of memory); the frame can be retried |
ScanResult
| Field | Type | Meaning |
|---|---|---|
status | Status | How processing of the frame ended |
codes | std::vector<Code> | The codes read, in reading order (see “Symbologies”). Empty if status is not Ok. No longer than Settings::maxCodes when that is greater than zero |
timedOut | bool | At least one engine was cut short by the frame time limit or never got to start. Success with the flag set is possible: a code was read, but some symbologies or hypotheses were not checked |
elapsedMs | double | How long the call took, ms. For scanFile() this includes loading the image |
Methods: bool ok() const — shorthand for status == Status::Ok; const std::string& text() const — the text of the first code or an empty string.
Code
| Field | Type | Meaning |
|---|---|---|
symbology | Symbology | The code's symbology — exactly one bit of the mask (see “Symbologies”) |
text | std::string | The symbol data, bytes as they are. A leading FNC1 (the GS1 flag) is not passed on; an FNC1 inside is the GS separator, byte 0x1D, not its printable form |
box | Box | Where the symbol was found in the original frame — a rectangle aligned with the frame axes |
corners | Quad | The symbol's four corners in the same frame, more precise than the box; the meaning of the indices depends on the symbology (see “Geometry”) |
moduleCount | int | DataMatrix: modules per symbol side; 0 — unknown or not a DataMatrix |
modulePx | float | Linear symbologies: module width in pixels; 0 — unknown or not a linear symbology |
Text encoding
Code::text holds the bytes the symbol carries; the library neither re-encodes them nor guesses their encoding.
- DataMatrix. Bytes 0x00–0x7F are ASCII. Bytes 0x80–0xFF (Upper Shift, Base 256 mode) are returned as they are. By the DataMatrix standard such bytes default to ISO 8859-1 (Latin-1), but many generators put UTF-8 text into Base 256, so the decision is yours. ECI is not supported: an ECI switch is not interpreted, and the text of a symbol with ECI may contain service bytes; macros 05/06 are not expanded into the
[)>header, and Structured Append is not assembled — each symbol is read on its own. Chestny ZNAK codes and GS1 DataMatrix use none of this: they contain only ASCII and the0x1Dseparator. - Code 128 and GS1-128. Code sets A, B and C plus SHIFT; the text is always 7-bit, 0x00–0x7F (code set A also yields control characters 0x00–0x1F). FNC2, FNC3 and FNC4 are not supported: a symbol containing them is not read — extended Latin-1 characters via FNC4 never appear in the text.
- EAN/UPC — digits only.
For display where UTF-8 is required: the text of linear symbologies is already ASCII; check DataMatrix text for valid UTF-8 and, if it is not, convert it from Latin-1 (each byte 0x80–0xFF is one character U+0080–U+00FF).
Multiple codes per frame
const unir::Scanner scanner(unir::Settings::multi(0)); // 0 — as many as found
for (const unir::Code& c : scanner.scan(frame).codes)
use(c.symbology, c.text, c.box);With one code (maxCodes = 1, the default) processing stops at the first code read — of any symbology in the mask. If a frame has a DataMatrix and an SSCC side by side and you need both, use multi(); if you need only one of them, narrow the mask (see “Symbologies”).
Symbologies
Each symbology is a separate bit: Settings::symbologies is a mask of them — what to look for — and Code::symbology is exactly one bit — what was read.
enum class Symbology : uint32_t {
None = 0,
DataMatrix = 1u << 0, // ECC200, ISO/IEC 16022 (dmr engine)
Code128 = 1u << 1, // Code 128 without the GS1 flag (ssccr engine)
Gs1_128 = 1u << 2, // GS1-128: Code 128 with a leading FNC1 (ssccr engine)
Ean13 = 1u << 3, // EAN/UPC — ssccr engine
Ean8 = 1u << 4,
UpcA = 1u << 5,
UpcE = 1u << 6,
};
using Symbologies = uint32_t; // symbology mask
constexpr Symbologies operator|(Symbology a, Symbology b);
constexpr Symbologies operator|(Symbologies a, Symbology b);
constexpr bool has(Symbologies mask, Symbology s);
constexpr Symbologies kEanUpc = Ean13 | Ean8 | UpcA | UpcE;
constexpr Symbologies kAllSymbologies = DataMatrix | Code128 | Gs1_128 | kEanUpc; // every symbology of this buildauto s = unir::Settings::stream();
s.symbologies = unir::Symbology::DataMatrix | unir::Symbology::Gs1_128; // marking codes and SSCC
const unir::Scanner scanner(s);| Symbology | What is read |
|---|---|
DataMatrix | DataMatrix ECC200 of any structure — by the same engine and with the same answers as DataMatrix Reader |
Gs1_128 | Code 128 with a leading FNC1. Without Code128 in the mask only SSCC (AI 00) is read; together with Code128, other GS1-128 as well |
Code128 | Code 128 without the GS1 flag. Acceptance is stricter than for SSCC: at least two scan lines must agree. Without Gs1_128 in the mask, SSCC and other GS1-128 are discarded |
Ean13, Ean8, UpcA, UpcE | Acceptance requires two agreeing scan lines and quiet zones. UPC-A is 12 digits (an EAN-13 whose first digit is 0), UPC-E is 8 digits (number system, 6 digits, check digit), not expanded to UPC-A. The engine reads all four together, but only those in the mask are returned |
By default Settings::symbologies is kAllSymbologies, i.e. everything this build can read. Future versions will add QR and other symbologies, and a program relying on the default will start looking for them after an update — and spend frame time on them. If you need a fixed set, set the mask explicitly.
Names and AIM prefixes
// Short name: "datamatrix", "code128", "gs1-128", "ean-13", "ean-8",
// "upc-a", "upc-e"; None — "none".
const char* nameOf(Symbology s);
// AIM symbology identifier (ISO/IEC 15424), without the data modifier:
// "]d", "]C", "]E".
const char* aimPrefixOf(Symbology s);
// Mask from a comma-separated list of names: "dm,128,gs1-128", "all".
// An unknown name returns false and leaves mask unchanged.
bool parseSymbologies(const std::string& list, Symbologies& mask);parseSymbologies accepts, case-insensitively: datamatrix | dm, code128 | 128, gs1-128 | gs1 | sscc, ean-13 | ean13, ean-8 | ean8, upc-a | upca, upc-e | upce, ean — all four EAN/UPC, and all. An empty string counts as an unknown name too. This is handy when the set of symbologies comes from a configuration file or the command line — the utility's --sym option uses the same parser.
aimPrefixOf returns the symbology identifier without a modifier: ]d for DataMatrix, ]C for Code 128 and GS1-128, ]E for EAN/UPC. In this version the GS1 flag (FNC1) of a DataMatrix is not reported, so there is no modifier such as ]d2: tell GS1-structured DataMatrix apart by its content. The modifier will be added once the engines start reporting the FNC1 flag.
Processing order and code order
The engines are called one after another, and the order depends on how many codes you ask for:
- One code (
maxCodes = 1): first a fast pass of the linear engine — SSCC and EAN/UPC, then DataMatrix, then other Code 128 (and other GS1-128), which is the most expensive on a frame without a barcode and therefore goes last. The first code read is the answer: the remaining engines are not called. - Several codes (
maxCodes0 or greater than 1): first all linear symbologies, then DataMatrix.codeslists the linear codes first, then DataMatrix; processing ends as soon asmaxCodeshave been collected.
Engines not covered by the mask are left out of the chain and cost no time. Before handing the frame to the DataMatrix engine, unir paints over the linear codes already read, together with their quiet zones, in a flat tone, so the bars do not send it into a search over false candidates. The mask is built from the corners of the codes read, does not touch other symbols and does not affect the coordinates in the result.
Frame
PixelFormat
enum class PixelFormat {
Gray8, // one channel: luminance; cheapest for the scanner
Bgr8, // three channels, OpenCV order
Rgb8,
Bgra8, // four channels, OpenCV and Windows order
Rgba8,
};
constexpr int channelsOf(PixelFormat f); // 1, 3 or 4For a monochrome camera, pass the frame as Gray8 rather than replicated into three channels.
ImageView
A view over a foreign buffer: the library does not copy it, free it or outlive it — the buffer must stay alive for the whole call. The fields mirror the frame view of dmr and ssccr, so the frame reaches the engines without copying.
struct ImageView {
const uint8_t* data = nullptr;
int width = 0;
int height = 0;
int stride = 0; // bytes per row; 0 — rows packed tightly
PixelFormat format = PixelFormat::Gray8;
bool empty() const;
int channels() const;
int rowBytes() const; // stride with the zero already resolved
const uint8_t* row(int y) const;
};stride = 0 means “rows packed tightly” (width * channels), not “zero bytes per row”: a camera frame is almost never tightly packed — rows are aligned, and a crop inherits the row length of the original. An empty view or a stride shorter than the row gives Status::InvalidImage; such a frame never reaches the engines.
Image
A frame owned by the library itself, rows packed tightly. It converts implicitly to ImageView, so it can be passed to scan() directly:
class Image {
public:
Image() = default;
Image(int width, int height, PixelFormat format);
void reset(int width, int height, PixelFormat format); // buffer of that size, filled with zeros
void clear();
int width() const;
int height() const;
int stride() const;
PixelFormat format() const;
bool empty() const;
uint8_t* data();
const uint8_t* data() const;
uint8_t* row(int y);
const uint8_t* row(int y) const;
ImageView view() const;
operator ImageView() const; // implicit — for calling scan(image)
};Reading from disk
struct ImageLimits {
int maxSide = 16384; // 0 — kHardMaxSide
long long maxPixels = 100'000'000; // 0 — kHardMaxPixels
static constexpr int kHardMaxSide = 65535;
static constexpr long long kHardMaxPixels = 1LL << 29;
bool allows(long long w, long long h) const;
};
enum class LoadStatus {
Ok, // image read
NotFound, // no such file
Unreadable, // the file exists but is not a readable image (or did not make it in time)
TooLarge, // size from the header exceeds ImageLimits; pixels were not read
};
enum class LoadAs {
Color, // PixelFormat::Bgr8
Gray, // PixelFormat::Gray8 — enough for the scanner
};
LoadStatus loadImage(const std::string& path, Image& out, LoadAs as = LoadAs::Gray,
const ImageLimits& limits = ImageLimits{});The image is decoded by the same decoders as in dmr — with size limits and protection against decompression bombs: JPEG and PNG, detected by the first bytes of the file, not by the extension. The path is UTF-8 (on Windows the system encoding works too, as with argv: a string that is not valid UTF-8 is read in it). By default the image is loaded as grayscale: the scanner does not need colour. The limits are checked against the file header before any memory is allocated; pass the same ones as in Settings::imageLimits. On failure out is left unchanged.
unir::Image frame;
switch (unir::loadImage("frame.jpg", frame)) {
case unir::LoadStatus::Ok: break;
case unir::LoadStatus::NotFound: /* no such file */ break;
case unir::LoadStatus::Unreadable: /* the file exists but is not an image */ break;
case unir::LoadStatus::TooLarge: /* exceeds the limits */ break;
}A standalone loadImage() runs without the frame time limit. When the image comes from an untrusted source, Scanner::scanFile() is the better choice: loading and reading run under one time limit there.
Geometry
struct Point {
float x = 0.0f;
float y = 0.0f;
};
struct Box { // rectangle aligned with the frame axes
int x = 0, y = 0, width = 0, height = 0;
bool empty() const;
};
struct Quad { // the symbol's four corners, in circular order
Point corner[4];
};All coordinates are in pixels of the original frame. Point is fractional: corners are located with sub-pixel precision. Box is a rectangle aligned with the frame axes; Quad gives the symbol's four corners more precisely than a rectangle can: on a conveyor the symbol is captured at an angle.
for (const unir::Point& p : c.corners.corner)
drawTo(p.x, p.y);The corners of a Quad always go in circular order, but which corner sits at which index depends on the symbology:
- DataMatrix — index 3 holds the corner the detector took for the corner of the solid L finder pattern.
corner[3]is fine for drawing an overlay on the frame, but not for drawing conclusions about how the symbol's content is rotated. - Code 128 and GS1-128 —
corner[0]is on the start side,corner[1]on the stop side along the same edge of the bars,corner[2]andcorner[3]on the opposite edge. They show the reading direction.
Check Code::symbology before interpreting the corners.
Settings
Start from a preset rather than individual fields — the preset's name already tells you which scenario it is tuned for. The preset sets the mode of the DataMatrix engine; the linear engine does not depend on the mode.
| Preset | Use case | What differs |
|---|---|---|
Settings::stream() | A frame stream from one camera — a conveyor. The default | mode = Stream: the DataMatrix engine accumulates a symbol size hint across frames; one code |
Settings::single() | One-off, unrelated images — a folder, manual checks | mode = Single: no hint; the DataMatrix engine tries every standard size if it could not measure the size; one code |
Settings::multi(n) | Several codes in one frame | mode = Multi, maxCodes = n (default 0 — as many as found) |
auto s = unir::Settings::stream();
s.symbologies = unir::Symbology::DataMatrix | unir::kEanUpc;
s.budgetMs = 120.0; // one time limit for all symbologies; 0 — no limit
const unir::Scanner scanner(s);All fields
// unir/Settings.h
struct Settings {
static Settings stream(); // frame stream from one camera (the default)
static Settings single(); // unrelated frames
static Settings multi(int maxCodes = 0); // up to maxCodes codes per frame (0 — all)
Symbologies symbologies = kAllSymbologies; // which symbologies to look for
int maxCodes = 1; // 1 — up to the first code, N — up to N, 0 — all
double budgetMs = 0.0; // frame time limit, ms, one for all symbologies; 0 — none
double timeoutMs = 10000.0; // safety net, ms; 0 — the 60 s ceiling
ImageLimits imageLimits{}; // 16384 per side, 100,000,000 pixels
enum class Mode { Stream, Single, Multi };
Mode mode = Mode::Stream; // mode of the DataMatrix engine
};The presets change only mode, and multi() also maxCodes; all other fields keep the defaults from the declaration.
| Field | Default | Meaning |
|---|---|---|
symbologies | kAllSymbologies | Which symbologies to look for — a mask (see “Symbologies”). The default is every symbology of the build; future versions will have more |
maxCodes | 1 | How many codes to look for: 1 — up to the first code of any symbology, N — up to N, 0 — all |
budgetMs | 0.0 | Time limit per frame, ms — one for all symbologies: the engines share it in turn, and an engine with no time left does not start at all (timedOut is set). 0 — no limit, only timeoutMs applies. The limit is soft: frame time may exceed it by the duration of the stage in progress. The DataMatrix engine receives the budget as is, and, as in dmr itself, a budget changes how it searches, not only when it stops |
timeoutMs | 10000.0 | A safety net, ms: the same cut-off as budgetMs (the smaller of the two applies), but it does not change how the engines work. 0 and anything above 60000 mean the 60 s ceiling: a frame is never processed for longer. Protection against hostile frames, not a latency setting |
imageLimits | 16384 per side, 100,000,000 pixels | The largest frame the scanner agrees to process (ImageLimits from unir/Image.h; 0 — the hard ceiling of 65535 and 2^29). A larger frame immediately gets Status::ImageTooLarge. scanFile() checks the same limits against the file header before decoding. A safeguard, not a quality setting |
mode | Mode::Stream | Mode of the DataMatrix engine: Stream — size hint across frames, Single — unrelated frames with a size search, Multi — several codes. Usually set by the preset |
unir does not expose the fine-grained dmr settings (rotation, GS1 check, candidate sources and the like): the DataMatrix engine runs with the dmr preset chosen by mode, plus your maxCodes, time limits and frame limits.
Speed: mode, mask, time limit
Frame time is determined by three settings, and all three are worth setting explicitly rather than left at their defaults.
- Mode. With one code (
stream(),single()) processing ends at the first code read, and the cheapest pass — SSCC and EAN/UPC — goes first: a frame with a single barcode is read quickly and DataMatrix is never reached.multi(0)cannot stop early: every engine in the mask goes through the whole frame, the linear one looks for every Code 128 and EAN/UPC, the DataMatrix engine tries all of its candidates. On a frame with one code this takes several times longer than one-code mode, and on a slow machine an order of magnitude longer. If you know how many codes to expect, usemulti(N): processing ends as soon as N have been collected. - Mask. Every symbology left out of the mask is an engine or a pass that does not run. The most expensive outcome is “nothing found” on a frame without a code: other
Code128is tried on anything that looks like bars, and the DataMatrix engine goes into a deep search. SSCC is read withoutCode128— withGs1_128alone. A mask of only the symbologies you need is noticeably faster than “all”, and a DataMatrix-only mask even more so. - Time limit.
budgetMsis one limit per frame for all symbologies. The engines spend it in turn; once it runs out, the running engine stops at its next stage, the following ones do not start, andtimedOutis set in the result. Codes read before the limit stay incodes. WithoutbudgetMsa frame is bounded only by thetimeoutMssafety net (10 s), which is noticeable formulti(0)on a hard frame.
In practice: on a conveyor, set the limit to the interval between frames minus your own processing; for a one-off image that a person is waiting for, in multi(), set it to as long as they are willing to wait, for example budgetMs = 3000. For scanFile() loading the file counts against the same limit.
Library version
#define UNIR_VERSION_MAJOR 1
#define UNIR_VERSION_MINOR 0
#define UNIR_VERSION_PATCH 1
#define UNIR_VERSION_STRING "1.0.1"
#define UNIR_VERSION_AT(ma, mi, pa) ((ma) * 10000 + (mi) * 100 + (pa))
#define UNIR_VERSION UNIR_VERSION_AT(UNIR_VERSION_MAJOR, UNIR_VERSION_MINOR, UNIR_VERSION_PATCH)
namespace unir {
std::string version(); // "1.0.1"
std::string engineVersions(); // engine versions inside the build: "dmr 1.0.12, ssccr 0.1.0"
}The macros describe the header you compiled against; unir::version() describes the library you linked against. They can diverge: the library is installed into a prefix and stays there longer than whoever built against it remembers. engineVersions() is for support requests and for comparing with DataMatrix Reader: it shows which revisions of dmr and ssccr the package was built with.
#if UNIR_VERSION >= UNIR_VERSION_AT(1, 1, 0)
// code for header 1.1.0 and newer
#endifLicense
The library reads codes while the trial period (30 days from the first run on the machine, no network or registration needed) or a license is in effect. Otherwise Scanner::scan() and scanFile() immediately return Status::Unlicensed: no engine is called and the image file is not opened. The license is bound to the hardware (motherboard, CPU, system disk), not to the OS installation: reinstalling Windows or replacing the disk or CPU keeps it, replacing the motherboard does not.
The unir trial period and license are its own, independent of DataMatrix Reader and DataMatrix Generator on the same machine: the license server is the same, but the product is different, and a license for one product does not unlock another.
#include <unir/License.h>
const unir::LicenseStatus s = unir::license::status();
if (!s.canScan()) show(s.message); // "The trial period has ended. A license is required"
unir::LicenseResult r = unir::license::activate(keyFromUser); // "UNIR-XXXXX-XXXXX-XXXXX-XXXXX"
if (!r.ok) show(r.message);Key file unir.key
The main way to activate is to put the key into a text file unir.key instead of calling activate() from your program. The name is its own, not license.key as with DataMatrix Reader: libraries of different products may sit in the same directory, and their keys differ. The library looks for the file, first match wins:
- next to the module that contains the library:
unir_c.dll/libunir_c.sofor the C ABI, the program itself for staticlibunir.aand theunirutility; - next to the program's executable.
The current directory and environment variables are not searched. Format: the first line that is not empty and not a # comment is the key, as issued — UNIR-XXXXX-XXXXX-XXXXX-XXXXX (case, spaces and dashes do not matter; a UTF-8 BOM is fine; files over 4 KB are ignored). The key is activated over the network automatically, with nothing to call: when the first Scanner is created or on the first license:: call — that call waits for the server once. If it fails, the reason is in status().message; without a network connection the attempt is repeated every hour, after a rejection by the server once a day, including across restarts. A file added while the program runs is picked up within an hour. A working license is not replaced by a file with a different key; a key released with deactivate() is not activated again from the file. license::keyFile() tells you which file the library found.
Where the trial and license state is kept
Windows: %ProgramData%\unir\license.dat, %LOCALAPPDATA%\unir\license.dat and the state value under the registry key HKCU\Software\unir. Linux: /var/lib/unir/license.dat and ~/.config/unir/license.dat of the user the process runs as (taken from the user database, not from $HOME). The library keeps several copies and takes the earliest trial start. A container has neither DMI data nor its own disk, so a license cannot be bound to it — run the program on the host or in a VM.
On Linux only root can create the shared copy in /var/lib/unir: under an ordinary user the directory is not created and that copy is silently skipped — the state lives in ~/.config/unir. This is not an error. The trial period starts on the first call into the library on the machine — the first Scanner or the first license::status() — and is written to disk right away, not when the program exits.
LicenseState
| Value | Meaning |
|---|---|
Trial | Trial period in progress |
Active | License is in effect |
RefreshDue | In effect, but there has been no contact with the server for a long time; auto-check retries hourly |
TrialExpired | Trial period over, no license |
Expired | The paid license term has ended; a renewal in your account is picked up by the auto-check |
NetworkRequired | Cannot continue without contacting the server; as soon as the server responds, the auto-check restores the license |
WrongMachine | The license was issued to a different machine |
Invalid | The license record fails signature verification |
LicenseStatus
// unir/License.h
enum class LicenseState {
Trial = 0, Active = 1, RefreshDue = 2, TrialExpired = 3,
Expired = 4, NetworkRequired = 5, WrongMachine = 6, Invalid = 7,
};
struct LicenseStatus {
LicenseState state = LicenseState::TrialExpired;
bool canScan() const; // Trial, Active or RefreshDue
int64_t validUntil = 0; // Unix seconds UTC
int64_t refreshAfter = 0; // 0 — never goes stale
int64_t offlineUntil = 0; // 0 — no limit
int daysLeft = 0;
std::string licenseId;
std::string activationId;
std::string offlineMode; // limited | extended | full; empty without a license
bool clockRollback = false;
std::string message; // the state in words, in Russian
};
struct LicenseResult {
bool ok = false;
std::string error; // failure code: LICENSE_NOT_FOUND, KEY_INVALID, NETWORK, ...
std::string message;
LicenseStatus status; // the state after the operation
};The C++ API has no “LicenseState → string” function: for the user there is message, and the machine-readable state code (trial, active, …) is given only by the utility's JSON, the C ABI and Python. If you need it in C++, a short switch with the same names will do:
const char* stateName(unir::LicenseState s) {
switch (s) {
case unir::LicenseState::Trial: return "trial";
case unir::LicenseState::Active: return "active";
case unir::LicenseState::RefreshDue: return "refresh_due";
case unir::LicenseState::TrialExpired: return "trial_expired";
case unir::LicenseState::Expired: return "expired";
case unir::LicenseState::NetworkRequired: return "network_required";
case unir::LicenseState::WrongMachine: return "wrong_machine";
case unir::LicenseState::Invalid: return "invalid";
}
return "invalid";
}| Field | Meaning |
|---|---|
state | LicenseState |
canScan() | Whether the library reads right now — true for Trial, Active or RefreshDue |
validUntil | End of the trial period or of the paid term, Unix seconds UTC |
refreshAfter | When the token is considered stale and the license moves to RefreshDue; 0 — never goes stale (trial period, full mode) |
offlineUntil | Beyond this time the license does not work without contacting the server; 0 — no limit (full mode) |
daysLeft | Whole days until the nearest of the limits above; 0 — limit passed |
licenseId / activationId | IDs of the license and of the specific activation |
offlineMode | limited | extended | full; empty without a license |
clockRollback | The system clock is behind a time the machine has already seen — rolling back the clock does not extend the term |
message | The state in words, for display to the user |
Functions that act on the license return a LicenseResult: ok, the failure code error (the server's answer as is — LICENSE_NOT_FOUND, ACTIVATION_LIMIT_REACHED, LICENSE_REVOKED and others — or the library's own: KEY_INVALID, NETWORK, WRONG_PRODUCT and others), message in words and status — the state after the operation.
Activation: online and offline
Online — a key from your account; the machine contacts the license server itself:
unir::LicenseResult r = unir::license::activate("UNIR-XXXXX-XXXXX-XXXXX-XXXXX");Offline — for a machine without internet access: the request file is carried to a machine with network access and uploaded to your account, which returns license.lic:
unir::license::writeActivationRequest("request.json"); // → upload to your account, get license.lic
unir::license::importLicenseFile("license.lic"); // the same call renews: the new file replaces the old oneA license activated online checks in with the server once a day in a background thread — refreshing the token (the limited and extended modes need this to keep working) and picking up revocation and term renewal. You don't need to call anything for this, and reading a frame never waits for the network; if the server is unreachable, it retries in an hour, and the license keeps working until the offline period runs out.
Moving to another machine and other functions
namespace unir::license {
void configure(const LicenseOptions& options); // before the first Scanner; optional
LicenseStatus status();
LicenseResult activate(const std::string& licenseKey);
LicenseResult refresh(); // check now, without waiting a day
LicenseResult deactivate(const std::string& reason = {}); // the receipt is sent to the server
LicenseResult deactivateToFile(const std::filesystem::path& receiptPath, const std::string& reason = {});
LicenseResult writeActivationRequest(const std::filesystem::path& path);
LicenseResult importLicenseFile(const std::filesystem::path& path);
std::string machineFingerprint(); // for support requests
std::filesystem::path keyFile(); // the unir.key found; empty — none
}
struct LicenseOptions {
std::string serverUrl; // empty — the one built in
std::string appVersion; // your program's version — shown in your account next to the machine
int timeoutMs = 15000; // how long to wait for one server request
bool autoCheck = true; // daily auto-check
};Call configure() before creating the first Scanner: the key from unir.key is activated on the first call into the library and takes the server and appVersion from here. Overriding the server is safe: responses are verified against signing keys built into the library. Turn off autoCheck only if your program must not go online on its own by its own rules; then limited and extended licenses are renewed by the program's own refresh() call. A license imported from a file has no key, so refresh() will not help it — it is renewed with a new license.lic.
To move to another machine, call deactivate() on the old one: the seat is released on the server with a receipt signed by that machine's secret; without a connection the license stays where it is. Without network access, use deactivateToFile(): the license is removed from the machine at once, and the receipt is written to a file that can be sent from any machine. All functions are thread-safe; functions that use the network block the caller until a response arrives or LicenseOptions::timeoutMs expires.
Every license server response is bound to its request: the library sends a random nonce, the server returns it in the signed response, and a previously recorded response will not be accepted again. The seat release receipt is single-use as well, so the server will not accept a receipt from a restored copy of the machine's old state. For licenses with full autonomy (full) releasing a seat consumes the transfer limit; once it is exhausted, transfers are handled by support. You can release a seat of a license yourself at most twice in 30 days in limited mode and at most once in extended mode; further transfers are handled by support.
The activation key is issued in your account after you purchase a plan in the catalog.
C ABI
The same library behind a flat C99 interface — header unir/unir_c.h, library unir_c.dll / libunir_c.so (how to link it — see “Installation”). For C, MSVC and any language that can call C functions (C#, Go, Delphi, Rust…). Rules of the boundary:
- Only plain C types cross it: integers,
double, pointers to buffers of known size, enums asintand opaque handles. A C++ exception never crosses it — an internal failure becomesNULLor an error code. - Ownership: everything a function returns (a handle, a string) belongs to the caller and is freed by its own function (
unir_image_free,unir_scan_result_free,unir_free_string; the scanner —unir_scanner_destroy). Pointers returned by accessors (unir_scan_result_code_textand the like) live no longer than the object they came from and are not freed separately. The string fromunir_symbology_nameis static and is not freed. unir_settings_tstarts withstruct_sizeand theabi_tagmarker: the caller sets both before the preset — in C the easiest way isunir_settings_t s = UNIR_SETTINGS_INIT;, in C++ —unir_settings_prepare(&s). A preset writes no further thanstruct_size; fields the caller does not know about (a newer library) keep their defaults. A wrong tag or an out-of-range size is rejected: the preset returns 0,unir_scanner_createreturnsNULL.- Symbologies are bits of the
UNIR_SYM_*mask, with the same values asunir::Symbology(see “Symbologies”); a decoded code carries exactly one bit (unir_scan_result_code_symbology), its name comes fromunir_symbology_name.UNIR_SYM_ALLmeans all symbologies of this build: later versions will add new ones (QR and others) to it, so set the mask explicitly if you need a fixed set. - Decoded text keeps the GS separator as the byte
0x1Dand may contain a zero byte — take the length fromunir_scan_result_code_text_len, notstrlen(). - Paths are UTF-8. On Windows the system encoding works too, as with
argv: a string that is not valid UTF-8 is read in it. Pass a name that is not in the Windows code page as UTF-8. - Statuses are the same as
unir::Status(see “Scan result”), plusUNIR_STATUS_INTERNAL_ERROR = -1for a failure of the C layer itself.unir_scanner_scanandunir_scanner_scan_filereturnNULLonly on an internal error; “no code” is a result with the statusUNIR_STATUS_NOT_FOUND. An image that failed to load givesUNIR_STATUS_IMAGE_TOO_LARGEorUNIR_STATUS_INVALID, with details in*load_status. - Do not call one scanner from several threads at once — use one scanner per thread.
- The license functions return the same JSON as
unir --license … --json(see “Command line”): one contract for the tool, the C ABI and Python.
Complete example: all codes in an image
#include <unir/unir_c.h>
#include <stdio.h>
/* All codes in an image: 0 — read, 1 — no code, 2 — error. */
int main(int argc, char** argv) {
if (argc < 2) return 2;
unir_settings_t settings = UNIR_SETTINGS_INIT; /* size and tag — before the preset */
unir_settings_multi(&settings, 0); /* all codes in the image */
settings.symbologies = UNIR_SYM_ALL; /* or, e.g., UNIR_SYM_DATAMATRIX | UNIR_SYM_EAN_UPC */
settings.budget_ms = 3000; /* one deadline for loading and all symbologies */
unir_scanner_t* scanner = unir_scanner_create(&settings);
if (!scanner) return 2;
unir_load_status_t load = UNIR_LOAD_UNREADABLE;
unir_scan_result_t* r = unir_scanner_scan_file(scanner, argv[1], &load);
int rc = 2;
if (r) { /* NULL — internal error */
unir_status_t st = unir_scan_result_status(r);
if (st == UNIR_STATUS_OK) {
for (int i = 0; i < unir_scan_result_code_count(r); ++i) {
printf("%s\t", unir_symbology_name(unir_scan_result_code_symbology(r, i)));
/* text may contain 0x1D (GS) and even 0x00 — use the length, not strlen() */
fwrite(unir_scan_result_code_text(r, i), 1, unir_scan_result_code_text_len(r, i), stdout);
putchar('\n');
}
rc = 0;
} else if (st == UNIR_STATUS_NOT_FOUND || st == UNIR_STATUS_NOT_DECODED) {
rc = 1;
} else if (st == UNIR_STATUS_UNLICENSED) {
char* why = unir_license_status_json(); /* {"state":"trial_expired",...} */
fprintf(stderr, "%s\n", why ? why : "unlicensed");
unir_free_string(why);
} else {
fprintf(stderr, "status %d, load %d\n", (int)st, (int)load); /* no file, not an image, too large */
}
unir_scan_result_free(r);
}
unir_scanner_destroy(scanner);
return rc;
}On Linux: cc main.c -o read $(pkg-config --cflags --libs unir_c), run with LD_LIBRARY_PATH=/unpacked/lib ./read label.png. It prints one line per code: the symbology, a tab and the text — for a Chestny ZNAK DataMatrix, with real 0x1D bytes between the elements.
Reference
All declarations of unir_c.h (ABI 1.0, library 1.0.1); comments are shortened — the full ones are in the header itself.
/* ── versions ── */
#define UNIR_C_ABI_VERSION_MAJOR 1
#define UNIR_C_ABI_VERSION_MINOR 0
size_t unir_c_abi_version(char* buf, size_t buf_size); /* "1.0" */
size_t unir_version(char* buf, size_t buf_size); /* library version, "1.0.1" */
size_t unir_engine_versions(char* buf, size_t buf_size); /* "dmr 1.0.12, ssccr 0.1.0" */
/* ── symbologies: mask bits ── */
#define UNIR_SYM_NONE 0x00u
#define UNIR_SYM_DATAMATRIX 0x01u
#define UNIR_SYM_CODE128 0x02u /* Code 128 without the GS1 flag */
#define UNIR_SYM_GS1_128 0x04u /* without UNIR_SYM_CODE128 in the mask — SSCC (AI 00) only */
#define UNIR_SYM_EAN13 0x08u
#define UNIR_SYM_EAN8 0x10u
#define UNIR_SYM_UPCA 0x20u /* 12 digits */
#define UNIR_SYM_UPCE 0x40u /* 8 digits, not expanded to UPC-A */
#define UNIR_SYM_EAN_UPC (UNIR_SYM_EAN13 | UNIR_SYM_EAN8 | UNIR_SYM_UPCA | UNIR_SYM_UPCE)
#define UNIR_SYM_ALL (UNIR_SYM_DATAMATRIX | UNIR_SYM_CODE128 | UNIR_SYM_GS1_128 | UNIR_SYM_EAN_UPC)
const char* unir_symbology_name(uint32_t symbology); /* "datamatrix" … "upc-e"; 0 — "none";
* a mask or an unknown bit — "unknown" */
int unir_parse_symbologies(const char* list, uint32_t* mask); /* "dm,128,ean", "all"; 1 — done,
* 0 — unknown name */
/* ── frame ── */
typedef enum { UNIR_PIXEL_GRAY8 = 0, UNIR_PIXEL_BGR8 = 1, UNIR_PIXEL_RGB8 = 2,
UNIR_PIXEL_BGRA8 = 3, UNIR_PIXEL_RGBA8 = 4 } unir_pixel_format_t;
typedef struct {
const uint8_t* data; /* caller's buffer, alive for the whole scan call */
int width;
int height;
int stride; /* bytes per row; 0 — tightly packed */
unir_pixel_format_t format;
} unir_image_view_t;
typedef struct unir_image unir_image_t; /* opaque, buffer owned by the library */
typedef struct unir_settings unir_settings_t; /* defined below */
typedef enum { UNIR_LOAD_COLOR = 0, UNIR_LOAD_GRAY = 1 } unir_load_as_t;
typedef enum { UNIR_LOAD_OK = 0, UNIR_LOAD_NOT_FOUND = 1, UNIR_LOAD_UNREADABLE = 2,
UNIR_LOAD_TOO_LARGE = 3, UNIR_LOAD_TIMED_OUT = 4,
UNIR_LOAD_INVALID_ARGUMENT = 5 } unir_load_status_t;
/* JPEG and PNG; GRAY — GRAY8, COLOR — BGR8; *out is NULL on failure */
unir_load_status_t unir_load_image(const char* path, unir_load_as_t as, unir_image_t** out);
unir_load_status_t unir_load_image_ex(const char* path, unir_load_as_t as,
const unir_settings_t* settings, /* NULL — defaults */
unir_image_t** out);
unir_image_view_t unir_image_view(const unir_image_t* image);
void unir_image_free(unir_image_t* image);
/* ── settings: same fields and defaults as unir::Settings ── */
typedef enum { UNIR_MODE_STREAM = 0, UNIR_MODE_SINGLE = 1, UNIR_MODE_MULTI = 2 } unir_mode_t;
struct unir_settings {
uint32_t struct_size; /* sizeof(unir_settings_t), set by the caller FIRST */
uint32_t abi_tag; /* UNIR_SETTINGS_TAG, also before the preset */
uint32_t symbologies; /* UNIR_SYM_* mask; presets use UNIR_SYM_ALL */
int max_codes; /* 1 — up to the first code of any symbology, N — up to N, 0 — all */
int mode; /* unir_mode_t */
double budget_ms; /* ONE deadline for all symbologies; 0 — no limit */
double timeout_ms; /* safety timeout, 10000 in presets; 0 — the 60 s ceiling */
int max_image_side; /* 0 — the 65535 ceiling; 16384 in presets */
int64_t max_image_pixels; /* 0 — the 2^29 ceiling; 100,000,000 in presets */
};
#define UNIR_SETTINGS_TAG 0x31524E55u /* 'U' 'N' 'R' '1' */
#define UNIR_SETTINGS_INIT { .struct_size = (uint32_t)sizeof(unir_settings_t), .abi_tag = UNIR_SETTINGS_TAG }
#define UNIR_SETTINGS_MAX_SIZE 4096u
static inline void unir_settings_prepare(unir_settings_t* s); /* zeroes, sets size and tag */
int unir_settings_stream(unir_settings_t* out); /* 1 — filled in; 0 — NULL, wrong */
int unir_settings_single(unir_settings_t* out); /* tag or size */
int unir_settings_multi(unir_settings_t* out, int max_codes); /* 0 — all codes */
/* ── scanner ── */
typedef struct unir_scanner unir_scanner_t; /* opaque; one scanner per thread */
unir_scanner_t* unir_scanner_create(const unir_settings_t* settings); /* NULL — stream() */
void unir_scanner_destroy(unir_scanner_t* scanner);
/* ── result ── */
typedef enum { UNIR_STATUS_OK = 0, UNIR_STATUS_NOT_FOUND = 1, UNIR_STATUS_NOT_DECODED = 2,
UNIR_STATUS_UNLICENSED = 3, UNIR_STATUS_IMAGE_TOO_LARGE = 4,
UNIR_STATUS_INVALID = 5, UNIR_STATUS_FAILED = 6,
UNIR_STATUS_INTERNAL_ERROR = -1 } unir_status_t;
typedef struct unir_scan_result unir_scan_result_t; /* opaque */
unir_scan_result_t* unir_scanner_scan(unir_scanner_t* scanner,
const unir_image_view_t* frame); /* NULL — internal error */
/* an image from disk and the codes in it under one scanner deadline; the load
* outcome goes to *load_status (NULL — not needed) */
unir_scan_result_t* unir_scanner_scan_file(unir_scanner_t* scanner, const char* path,
unir_load_status_t* load_status);
void unir_scan_result_free(unir_scan_result_t* result);
unir_status_t unir_scan_result_status(const unir_scan_result_t* result);
int unir_scan_result_timed_out(const unir_scan_result_t* result);
double unir_scan_result_elapsed_ms(const unir_scan_result_t* result);
int unir_scan_result_code_count(const unir_scan_result_t* result);
uint32_t unir_scan_result_code_symbology(const unir_scan_result_t* result, int index); /* a single bit */
const char* unir_scan_result_code_text(const unir_scan_result_t* result, int index);
size_t unir_scan_result_code_text_len(const unir_scan_result_t* result, int index);
void unir_scan_result_code_box(const unir_scan_result_t* result, int index,
int* x, int* y, int* width, int* height);
void unir_scan_result_code_corners(const unir_scan_result_t* result, int index,
float corners_xy[8]); /* x0,y0 … x3,y3 */
int unir_scan_result_code_module_count(const unir_scan_result_t* result, int index); /* DataMatrix */
float unir_scan_result_code_module_px(const unir_scan_result_t* result, int index); /* linear codes */
/* ── license: JSON as from "unir --license … --json"; free with unir_free_string, NULL — internal error ── */
void unir_license_configure(const char* server_url, const char* app_version,
int timeout_ms, int auto_check); /* timeout_ms <= 0 — 15000 */
char* unir_license_status_json(void);
char* unir_license_activate_json(const char* key);
char* unir_license_refresh_json(void);
char* unir_license_deactivate_json(const char* reason); /* reason may be NULL */
char* unir_license_deactivate_to_file_json(const char* path, const char* reason);
char* unir_license_write_activation_request_json(const char* path);
char* unir_license_import_license_file_json(const char* path);
char* unir_license_fingerprint_json(void); /* {"fingerprint":"hex"} */
void unir_free_string(char* s);Command line
The package includes the unir utility alongside the library — for quick checks without writing code: a single image, a list of paths, a folder or a stream for an external service. Modes and formats are the same as those of the dmr utility, plus a symbology column.
unir <image> [options]
unir --batch <list> [options] list of paths, one per line
unir --folder <dir> [options] every image in the directory (not recursive)
unir --serve [options] one path per line from stdin
unir --license <subcommand> license
unir --version versions of unir and its engines| Option | Meaning |
|---|---|
--sym LIST | Comma-separated symbologies, names as in parseSymbologies: dm, 128, gs1 (sscc), ean (ean-13, ean-8, upc-a, upc-e), all — the default |
--codes N | How many codes to look for: 1 — the default, 0 — all; any value other than 1 selects the multi(N) preset |
--single | Images are unrelated — the single() preset; the default is a stream, stream() |
--budget-ms N | Time limit per image, ms, one for all symbologies; 0 — no limit |
--timeout-ms N | Safety net, ms (10000); 0 — the 60 s ceiling |
--max-side N / --max-pixels N | Largest image side (16384) and area (100000000) |
--color | Load the image in colour; grayscale by default |
--corners | In batch modes — the symbol's corners as a sixth column |
--root D | In --serve: read only images inside directory D |
Single image
$ unir label.jpg
processing: 24.9 ms
code: datamatrix 0104627191145677215n5Gr,pHTGIU&<GS>93CuXUThe utility prints in the system language (English on any non-Russian system; --lang ru|en or the LIBSCANNER_LANG variable sets it explicitly; the same variable sets the language of status().message and library error messages): processing is the processing time, and there is one code: <symbology> <text> line per code, with the symbology as given by nameOf(). In the text GS is shown as <GS>, TAB as <TAB>, line breaks as spaces. If the time limit cut the search short, a line budget: exhausted, search cut off follows. Errors go to stderr. The outcome is the exit code. The numbers are the same as in unir::Status, with one difference: an image that failed to load (no such file, not JPEG/PNG, decoding did not fit into the time limit) gives 1, not 5 (InvalidImage) as scanFile() would return; an oversized image gives 4, the same as ImageTooLarge. A time limit that cuts short the scan of an image already loaded does not change the code: it is 1 or 2 (or 0 if something was read in time) plus the budget line. Invalid command-line options also give 1.
| Code | Meaning |
|---|---|
| 0 | At least one code read (also when the time limit cut the search short but codes were found) |
| 1 | No code found; also no such file, not JPEG/PNG, decoding ran out of time, invalid options; in --serve — a path outside the --root directory |
| 2 | A candidate was found but not read — this does not mean the image holds a code (see “What NotDecoded really means”) |
| 3 | No trial period and no license |
| 4 | Image exceeds the limit |
| 5 | Inconsistent frame; the utility does not return it for images from disk — a file that failed to load gives 1 |
| 6 | Internal error; in --serve also an invalid symbology list in the request |
Batch mode and streaming
--batch and --folder run one process for the whole set of images known in advance and print results line by line to stdout, one line per code:
path<TAB>exit code<TAB>ms<TAB>text<TAB>symbology[<TAB>corners]The first four columns are the same as those of the dmr utility, so a parser for its output works here too. An image without a code gives one line with empty text and no symbology column; the second column holds the codes from the table above. --corners adds a sixth column x0,y0 x1,y1 x2,y2 x3,y3. The summary goes to stderr. Without a license the run stops at the first image with code 3: the rest would answer the same.
--serve uses the same line format, but the images are not known in advance: paths arrive one at a time, a line from stdin each, and are processed immediately — the process keeps the scanner alive between requests of an external service instead of paying for initialization on each one. After the lines of each image comes a #END# line, and the output is flushed at once — it marks the end of the answer even when there are several codes. A request line is either a path or path<TAB>sym=LIST: the symbologies for this image, in the same form as --sym; each set gets its own scanner with its own DataMatrix hint. Without a license the service answers with the image's line carrying code 3, then #END#, and exits with code 3. The process ends at the end of input.
$ mkfifo queue
$ unir --serve --codes 0 < queue &
$ printf '/path/to/image.jpg\tsym=dm,gs1\n' > queue
/path/to/image.jpg 0 91.24 0104627191145677215n5Gr,pHTGIU&<GS>93CuXU datamatrix
#END#License from the command line
unir --license status [--json]
unir --license activate <key> [--server URL] [--json]
unir --license refresh [--server URL] [--json]
unir --license request <request.json>
unir --license import <license.lic>
unir --license deactivate [<file>] [--server URL] [--json]
unir --license fingerprint [--json]request writes an offline activation request file, import accepts a license.lic (renewals too), and deactivate releases the machine over the network when given no file, or writes a receipt to be sent from another machine when given one. Exit codes: status returns 0 if unir reads and 3 if it does not; the other subcommands return 0 (done) or 1 (failed).
The --json flag gives the same result as a single JSON line on stdout — for scripts and admin panels. The fields, their order and the exit codes are exactly those of dmr --license … --json, so a service that already parses dmr's output can call unir instead:
$ unir --license status --json
{"state":"trial","can_scan":true,"valid_until":1792831857,"refresh_after":0,
"offline_until":0,"days_left":28,"license_id":"","activation_id":"",
"offline_mode":"","clock_rollback":false,"message":"Trial period: 28 days left"}Operations (activate, refresh, deactivate) answer with an object {"ok":…,"error":…,"message":…,"status":{…}}, where status is the state in the same form as above; fingerprint gives {"fingerprint":"…"}. The state field is a stable machine-readable code (trial, active, refresh_due, trial_expired, expired, network_required, wrong_machine, invalid) — part of the output format; it is never translated or reworded. The message field is in Russian.
The library lacks a feature you need, or your case is beyond its limits? We will adapt it to your task. Contact us →