Developer documentation

Machine-Readable Code Reader

One call reads DataMatrix, GS1-128 (SSCC), Code 128, EAN-13, EAN-8, UPC-A and UPC-E in a frame — Chestny ZNAK marking, logistics labels and retail barcodes.

7 symbologiesDataMatrix, GS1-128, Code 128, EAN-13, EAN-8, UPC-A and UPC-E — in one frame with one call
99.6%of 8,959 production-line frames with DataMatrix read — frame for frame the same as DataMatrix Reader
0 falsebarcodes over the same 8,959 frames: a linear code is accepted only when several scan lines agree

For AI assistants: this page in Markdown · all documentation in one file · llms.txt

Version 1.0

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 licenses

In “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 programWhat 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 programDynamic linking — C ABI, unir_c
Pythonpip 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).

MinGW-w64 GCC required, not MSVC

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-pkgconf

Include 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 unir

The 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 pkg-config query must be static

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 program

On 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_c

This 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.def

then, 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.lib

At 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 build

The 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 build

For 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.

HeaderContents
unir/Unir.hUmbrella header — includes everything below
unir/Image.hImageView, Image, PixelFormat, ImageLimits, loadImage
unir/Scanner.hScanner, ScanResult, Code, Status
unir/Settings.hScanner settings and presets
unir/Symbology.hSymbology, the Symbologies mask, names and AIM prefixes
unir/Geometry.hPoint, Box, Quad — where the code is located in the frame
unir/License.hLicense check and activation, the unir::license namespace
unir/Version.hVersion 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());
One Scanner per thread

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*loadstatus, timedOutC ABI / Python
ReadOkoutcome of the scanUNIR_LOAD_OK / OK
No such fileNotFoundInvalidImage, falseUNIR_LOAD_NOT_FOUND / NOT_FOUND
Not JPEG/PNG, or damagedUnreadableInvalidImage, falseUNIR_LOAD_UNREADABLE / UNREADABLE
Decoding did not fit into the time limitUnreadableInvalidImage, trueUNIR_LOAD_TIMED_OUT / TIMED_OUT
Exceeds the limits according to the headerTooLargeImageTooLarge, falseUNIR_LOAD_TOO_LARGE / TOO_LARGE
No license — the file was not openedOkUnlicensed, falseUNIR_LOAD_OK / OK
Errors are returned, not thrown

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.

Move-only

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:

StatusOutcomeWhat to do
OkCode presentUse codes. With timedOut, some symbologies or codes may have gone unchecked
NotFoundNo codeNext frame. With timedOut — not final
NotDecodedNo code (not read)Next frame or a new shot; do not assume a code is present
UnlicensedErrorNo license: show license::status().message; retrying the frame is pointless until activation
ImageTooLargeInput errorDownscale the frame or raise imageLimits
InvalidImageInput errorFrom scan() — a mistake in the frame description (your code); from scanFile() — the file was not read, the reason is in *load
FailedErrorInternal failure (for example, out of memory); the frame can be retried

ScanResult

FieldTypeMeaning
statusStatusHow processing of the frame ended
codesstd::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
timedOutboolAt 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
elapsedMsdoubleHow 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

FieldTypeMeaning
symbologySymbologyThe code's symbology — exactly one bit of the mask (see “Symbologies”)
textstd::stringThe 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
boxBoxWhere the symbol was found in the original frame — a rectangle aligned with the frame axes
cornersQuadThe symbol's four corners in the same frame, more precise than the box; the meaning of the indices depends on the symbology (see “Geometry”)
moduleCountintDataMatrix: modules per symbol side; 0 — unknown or not a DataMatrix
modulePxfloatLinear 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 the 0x1D separator.
  • 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 build
auto s = unir::Settings::stream();
s.symbologies = unir::Symbology::DataMatrix | unir::Symbology::Gs1_128;   // marking codes and SSCC
const unir::Scanner scanner(s);
SymbologyWhat is read
DataMatrixDataMatrix ECC200 of any structure — by the same engine and with the same answers as DataMatrix Reader
Gs1_128Code 128 with a leading FNC1. Without Code128 in the mask only SSCC (AI 00) is read; together with Code128, other GS1-128 as well
Code128Code 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, UpcEAcceptance 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
The default is every symbology of the build

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 (maxCodes 0 or greater than 1): first all linear symbologies, then DataMatrix. codes lists the linear codes first, then DataMatrix; processing ends as soon as maxCodes have 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 4

For 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);
Index meaning depends on the symbology

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] and corner[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.

PresetUse caseWhat differs
Settings::stream()A frame stream from one camera — a conveyor. The defaultmode = Stream: the DataMatrix engine accumulates a symbol size hint across frames; one code
Settings::single()One-off, unrelated images — a folder, manual checksmode = 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 framemode = 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.

FieldDefaultMeaning
symbologieskAllSymbologiesWhich symbologies to look for — a mask (see “Symbologies”). The default is every symbology of the build; future versions will have more
maxCodes1How many codes to look for: 1 — up to the first code of any symbology, N — up to N, 0 — all
budgetMs0.0Time 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
timeoutMs10000.0A 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
imageLimits16384 per side, 100,000,000 pixelsThe 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
modeMode::StreamMode 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, use multi(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 Code128 is tried on anything that looks like bars, and the DataMatrix engine goes into a deep search. SSCC is read without Code128 — with Gs1_128 alone. A mask of only the symbologies you need is noticeably faster than “all”, and a DataMatrix-only mask even more so.
  • Time limit. budgetMs is 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, and timedOut is set in the result. Codes read before the limit stay in codes. Without budgetMs a frame is bounded only by the timeoutMs safety net (10 s), which is noticeable for multi(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
#endif

License

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:

  1. next to the module that contains the library: unir_c.dll / libunir_c.so for the C ABI, the program itself for static libunir.a and the unir utility;
  2. 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

ValueMeaning
TrialTrial period in progress
ActiveLicense is in effect
RefreshDueIn effect, but there has been no contact with the server for a long time; auto-check retries hourly
TrialExpiredTrial period over, no license
ExpiredThe paid license term has ended; a renewal in your account is picked up by the auto-check
NetworkRequiredCannot continue without contacting the server; as soon as the server responds, the auto-check restores the license
WrongMachineThe license was issued to a different machine
InvalidThe 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";
}
FieldMeaning
stateLicenseState
canScan()Whether the library reads right now — true for Trial, Active or RefreshDue
validUntilEnd of the trial period or of the paid term, Unix seconds UTC
refreshAfterWhen the token is considered stale and the license moves to RefreshDue; 0 — never goes stale (trial period, full mode)
offlineUntilBeyond this time the license does not work without contacting the server; 0 — no limit (full mode)
daysLeftWhole days until the nearest of the limits above; 0 — limit passed
licenseId / activationIdIDs of the license and of the specific activation
offlineModelimited | extended | full; empty without a license
clockRollbackThe system clock is behind a time the machine has already seen — rolling back the clock does not extend the term
messageThe 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 one

A 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.

Activation key

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 as int and opaque handles. A C++ exception never crosses it — an internal failure becomes NULL or 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_text and the like) live no longer than the object they came from and are not freed separately. The string from unir_symbology_name is static and is not freed.
  • unir_settings_t starts with struct_size and the abi_tag marker: the caller sets both before the preset — in C the easiest way is unir_settings_t s = UNIR_SETTINGS_INIT;, in C++ — unir_settings_prepare(&s). A preset writes no further than struct_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_create returns NULL.
  • Symbologies are bits of the UNIR_SYM_* mask, with the same values as unir::Symbology (see “Symbologies”); a decoded code carries exactly one bit (unir_scan_result_code_symbology), its name comes from unir_symbology_name. UNIR_SYM_ALL means 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 0x1D and may contain a zero byte — take the length from unir_scan_result_code_text_len, not strlen().
  • 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”), plus UNIR_STATUS_INTERNAL_ERROR = -1 for a failure of the C layer itself. unir_scanner_scan and unir_scanner_scan_file return NULL only on an internal error; “no code” is a result with the status UNIR_STATUS_NOT_FOUND. An image that failed to load gives UNIR_STATUS_IMAGE_TOO_LARGE or UNIR_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
OptionMeaning
--sym LISTComma-separated symbologies, names as in parseSymbologies: dm, 128, gs1 (sscc), ean (ean-13, ean-8, upc-a, upc-e), all — the default
--codes NHow many codes to look for: 1 — the default, 0 — all; any value other than 1 selects the multi(N) preset
--singleImages are unrelated — the single() preset; the default is a stream, stream()
--budget-ms NTime limit per image, ms, one for all symbologies; 0 — no limit
--timeout-ms NSafety net, ms (10000); 0 — the 60 s ceiling
--max-side N / --max-pixels NLargest image side (16384) and area (100000000)
--colorLoad the image in colour; grayscale by default
--cornersIn batch modes — the symbol's corners as a sixth column
--root DIn --serve: read only images inside directory D

Single image

$ unir label.jpg
processing: 24.9 ms
code: datamatrix 0104627191145677215n5Gr,pHTGIU&<GS>93CuXU

The 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.

CodeMeaning
0At least one code read (also when the time limit cut the search short but codes were found)
1No code found; also no such file, not JPEG/PNG, decoding ran out of time, invalid options; in --serve — a path outside the --root directory
2A candidate was found but not read — this does not mean the image holds a code (see “What NotDecoded really means”)
3No trial period and no license
4Image exceeds the limit
5Inconsistent frame; the utility does not return it for images from disk — a file that failed to load gives 1
6Internal 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 →