Developer documentation

DataMatrix Reader

Reads GS1 DataMatrix codes from industrial camera frames — for conveyor lines marked under Chestny ZNAK, the Russian product marking system.

100%of frames read correctly (8,773 readable frames from a conveyor dataset)
from 5.3 msminimum time per frame; median over the dataset is 28 ms
Version 1.0

Overview

The library reads DataMatrix (ECC200) codes from production-line camera frames — for example, boxes carrying Chestny ZNAK marking codes (the Russian national product marking system), where the symbol spans about a hundred pixels in the frame. The reader is designed for low capture resolution — around two pixels per symbol module — and verifies every geometry hypothesis with Reed-Solomon error correction. 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/dmr/. A frame is passed as a view over a foreign buffer (dmr::ImageView) — no copying, straight from the camera buffer or image you already have.

Scope

By default the scanner reads DataMatrix of any structure. The requireGs1 mode (see “Settings”) additionally checks the content format — AI 01 (GTIN, 14 digits with a valid check digit), then AI 21 — and rejects symbols that do not match the GS1 structure.

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 libdmr.a is linked into your program in full, or dynamically — through the dmr_c library with a flat C interface (C ABI). The package contents are the same on both OSes:

include\dmr\{Dmr,Diagnostics,Geometry,Image,License,Scanner,Settings,Version}.h   C++ API
include\dmr\dmr_c.h                                                               C ABI
lib\libdmr.a                 C++ static library
lib\pkgconfig\dmr.pc
bin\dmr_c.dll                C ABI library
lib\dmr_c.dll.a              its import library (MinGW)
lib\pkgconfig\dmr_c.pc
bin\dmr.exe                  command-line utility
share\doc\dmr\licenses\       third-party licenses
Your programWhat to use
C++, built with MinGW-w64 GCC (Windows) or GCC (Linux)Static linking — C++ API, libdmr.a
MSVC or another compiler; another language (C, C#, Delphi, Go…); updating the library without rebuilding the programDynamic linking — C ABI, dmr_c
PythonThe wheel from “Downloads” — Python documentation

Static linking — C++ API

libdmr.a is linked into your program in full: nothing needs to sit next to it except the key file license.key (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 — libdmr.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 dmr_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 dmr.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 dmr

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:

dmr_dep = dependency('dmr', required: true, static: true)
executable('myprogram', 'main.cpp', dependencies: [dmr_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

dmr_c is the same library behind a flat C interface (header dmr/dmr_c.h): dmr_* 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, strings) is freed by its own functions: dmr_scan_result_free, dmr_image_free, dmr_free_string.

#include <dmr/dmr_c.h>
#include <stdio.h>

int main(void) {
    char v[64];
    dmr_version(v, sizeof v);
    printf("dmr %s\n", v);

    dmr_scanner_t* s = dmr_scanner_create(NULL);   /* NULL — default settings */
    /* ... dmr_scanner_scan(s, ...) ... */
    dmr_scanner_destroy(s);
    return 0;
}

MinGW-w64 GCC — via dmr_c.pc and the import library lib\dmr_c.dll.a, without --static:

$env:PKG_CONFIG_PATH = "C:\path\to\unpacked\lib\pkgconfig"
pkg-config --cflags --libs dmr_c

This yields -I…\include -L…\lib -ldmr_c. Contents of dmr_c.pc:

prefix=${pcfiledir}/../..
includedir=${prefix}/include
libdir=${prefix}/lib

Name: dmr_c
Version: 1.0.0
Libs: -L${libdir} -ldmr_c
Cflags: -I${includedir}

MSVC — the package has no .lib file; you generate it from the DLL itself. In the MSYS2 shell, extract the export definitions:

gendef dmr_c.dll          # package mingw-w64-x86_64-tools; writes dmr_c.def

then, in the Developer Command Prompt for VS (x64), create the import library and build:

lib /def:dmr_c.def /machine:x64 /out:dmr_c.lib
cl /I C:\path\to\unpacked\include main.c dmr_c.lib

At run time dmr_c.dll must be next to your .exe (or in a directory on PATH), and the key file license.key must be next to dmr_c.dll.

In meson — likewise by package name, without static:

dmr_c_dep = dependency('dmr_c', required: true)
executable('myprogram', 'main.c', dependencies: [dmr_c_dep])

The Python wheel is a wrapper over dmr_c, with the library bundled inside the package: Python documentation.

Quick start

A minimal program: load an image from disk, read it, print the text. All you need is one header:

#include <dmr/Dmr.h>
#include <iostream>

int main() {
    dmr::Image frame;
    if (dmr::loadImage("frame.jpg", frame) != dmr::LoadStatus::Ok) {
        std::cerr << "could not read the file\n";
        return 1;
    }

    dmr::Scanner scanner;                          // frame stream of one label
    const dmr::ScanResult r = scanner.scan(frame);  // Image converts implicitly to ImageView

    if (r.ok()) {
        std::cout << r.text() << "\n";
    } else if (r.status == dmr::Status::NotFound) {
        std::cout << "no symbol in the frame\n";
    } else {
        std::cout << "symbol found but could not be read\n";
    }
}

The example can be built with Meson. Next to main.cpp put the unpacked package dmr-1.0.0-windows-x64 and a meson.build file:

project('test_dmr', 'cpp',
  version : '0.1.0',
  default_options : ['cpp_std=c++20', 'warning_level=3', 'buildtype=release'])

cxx = meson.get_compiler('cpp')

# The prebuilt dmr sits next to the project. The dependency is declared by hand,
# so pkg-config is not needed at all (in 1.0.0 archives downloaded before
# 2026-10-04 the .pc paths pointed at the build machine; this way works with them too).
dmr_root = meson.current_source_dir() / 'dmr-1.0.0-windows-x64'

dmr_lib = cxx.find_library('dmr', dirs : dmr_root / 'lib', static : true)

# Libs.private from dmr.pc — required for static linking
dmr_private = []
foreach l : ['winhttp', 'crypt32', 'shell32', 'advapi32']
  dmr_private += cxx.find_library(l)
endforeach

dmr_dep = declare_dependency(
  include_directories : include_directories('dmr-1.0.0-windows-x64/include'),
  dependencies : [dmr_lib] + dmr_private)

executable('test_dmr', 'main.cpp',
  dependencies : dmr_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 via pkg-config: the package's .pc files are relocatable, so instead of the manual declaration one line in meson.build is enough — dmr_dep = dependency('dmr', static : true) — plus the path to the unpacked lib/pkgconfig at setup:

meson setup build --pkg-config-path="$PWD/dmr-1.0.0-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 dmr::ImageView view{ m.data, m.cols, m.rows, (int)m.step, dmr::PixelFormat::Bgr8 };
const dmr::ScanResult r = scanner.scan(view);

Headers

There are seven public headers, all under include/dmr/; the eighth, dmr/Dmr.h, includes them all at once.

HeaderContents
dmr/Dmr.hUmbrella header — includes everything below
dmr/Image.hImageView, Image, PixelFormat, loadImage, paths
dmr/Scanner.hScanner, ScanResult, Code, Status
dmr/Settings.hScanner settings and presets
dmr/Geometry.hPoint, Box, Quad — where the code is located in the frame
dmr/Diagnostics.hexplain(), log sink (LogSink)
dmr/License.hLicense check and activation, namespace dmr::license
dmr/Version.hHeader version (macros) and built library version (dmr::version())

Headers depend on each other no more than necessary: Settings.h and Scanner.h pull in <functional> and <vector>, while code that only builds a frame view needs just Image.h.

Scanner

Scanner is an object, not a function, and you should not create one per frame: the symbol size hint (Settings::mcHintRun) accumulates over the frames of a stream and must outlive them, and the thread pool size is a per-process setting. Still, scan() is declared const and stores nothing in the object except that hint: the result depends on the frame, not on the history of reads. Candidates are tried in parallel internally — there is no need to run your own thread pool on top of scan().

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 the codes in a frame.
    //   frame        the whole frame, color or grayscale
    //   moduleCount  symbol side in modules; 0 — detect automatically
    ScanResult scan(ImageView frame, int moduleCount = 0) const;

    // Symbol size suggested by previous frames; 0 — no hint.
    int moduleCountHint() const;

    // Forget the hint.
    void resetStream();

    const Settings& settings() const;
};

// One-off read without an object: a probe, a script, a single file. The size
// hint from a frame stream does not apply here — there is nothing to accumulate it in.
ScanResult scan(ImageView frame, const Settings& s = Settings::single());

When the label on the conveyor changes, reset the hint explicitly instead of waiting for it to fade on its own:

scanner.resetStream();
Move-only

Scanner can be moved (Scanner&&), but not copied — the copy constructor and copy assignment operator are explicitly deleted.

Scan result

Status

enum class Status {
    Ok         = 0,  // at least one code was read
    NotFound   = 1,  // no symbol found in the frame
    NotDecoded = 2,  // symbol found but not read
    Unlicensed = 3,  // frame not processed: no trial period and no license
};

With Status::Unlicensed the frame was not processed at all — the reason is given by dmr::license::status() (see “License”).

ScanResult

FieldTypeMeaning
statusStatusHow processing of the frame ended
codesstd::vector<Code>Codes read, one per symbol, in reading order: top to bottom, left to right for equal top edges. Empty if status is not Ok. With Settings::maxCodes = 1 the length is at most one
timedOutboolThe search was cut short by Settings::budgetMs. Set only when the budget actually cut something off — success with the flag set is possible: a code was read, but some hypotheses were not checked
elapsedMsdoubleFrame processing time, ms, excluding loading the image from disk
detailsstd::shared_ptr<const Details>Diagnostic breakdown; empty unless requested via Settings::collectDetails

Methods: bool ok() const — shorthand for status == Status::Ok; const std::string& text() const — the text of the first code or an empty string, so you don't have to write codes.empty() ? "" : codes[0].text on every call.

Code

FieldTypeMeaning
textstd::stringThe decoded string. Bytes as is: GS separators stay as the byte 0x1D and are not replaced with a printable form
boxBoxWhere the symbol was found in the source frame — the candidate rectangle
cornersQuadThe four symbol corners in the same frame, more precise than the box (see “Geometry”)
moduleCountintSymbol side in modules

box is not just decoration: two candidates with the same string are one symbol seen twice if their boxes overlap, and two identical labels if they don't.

Multiple codes per frame

const dmr::Scanner scanner(dmr::Settings::multi(0));   // 0 — as many as found
for (const dmr::Code& c : scanner.scan(frame).codes)
    use(c.text, c.box, c.moduleCount);

Frame

PixelFormat

enum class PixelFormat {
    Gray8,   // single channel: luminance; cheapest for the scanner
    Bgr8,    // three channels, OpenCV order
    Rgb8,    // three channels, the order most others use
    Bgra8,   // four channels, OpenCV and Windows order
    Rgba8,   // four channels
};

The scanner works on luminance and converts a color frame to it first. For a monochrome camera, pass the frame as Gray8 rather than replicated into three channels — the result is the same, and the frame is processed faster.

ImageView

A view over a foreign buffer: the library does not copy it, free it or keep it beyond the call — the buffer must stay alive for the whole call.

struct ImageView {
    const uint8_t* data   = nullptr;
    int            width  = 0;
    int            height = 0;
    int            stride = 0;              // bytes per row; 0 — tightly packed
    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 “tightly packed” (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. Built at the call site without copying:

const dmr::ImageView view{ m.data, m.cols, m.rows, (int)m.step, dmr::PixelFormat::Bgr8 };

Image

A frame owned by the library itself — needed where the library creates it, i.e. when reading from disk. It converts implicitly to ImageView, so it is passed to scan() directly, without .view():

class Image {
public:
    Image() = default;
    Image(int width, int height, PixelFormat format);

    void reset(int width, int height, PixelFormat format);  // tightly packed layout
    void clear();

    int         width()    const;
    int         height()   const;
    int         stride()   const;
    PixelFormat format()   const;
    int         channels() const;
    bool        empty()    const;

    uint8_t*       data();
    uint8_t*       row(int y);

    ImageView view() const;
    operator ImageView() const;   // implicit — for calling scan(image)
};

Reading from disk

enum class LoadStatus {
    Ok,          // image read
    NotFound,    // no such file
    Unreadable,  // the file exists but is not a readable image
};

enum class LoadAs {
    Color,  // three channels, PixelFormat::Bgr8
    Gray,   // one channel, PixelFormat::Gray8
};

LoadStatus loadImage(const std::string& path, Image& out, LoadAs as = LoadAs::Color);

For the scanner, Gray is the best choice: an image from a monochrome camera (grayscale in a three-channel JPEG) is cheaper to read this way, and the result is byte-for-byte the same, because each Color channel is exactly that luminance. On failure out is cleared — half a frame from a previous attempt is worse than an empty one.

dmr::Image frame;
switch (dmr::loadImage("frame.jpg", frame, dmr::LoadAs::Gray)) {
    case dmr::LoadStatus::Ok:         break;
    case dmr::LoadStatus::NotFound:   /* no such file */ break;
    case dmr::LoadStatus::Unreadable: /* the file exists but is not an image */ break;
}
Path encoding

loadImage handles the system path encoding itself — a directory with a non-ASCII (e.g. Cyrillic) name does not break reading. For your own path handling the header provides toPath(const std::string&), fromPath(const std::filesystem::path&) (the reverse conversion) and printablePath(...) — the path in UTF-8, for logging.

Geometry

struct Point {
    float x = 0.0f;
    float y = 0.0f;
};

struct Box {
    int x = 0, y = 0, width = 0, height = 0;
    bool empty() const;
};

struct Quad {
    Point corner[4];
};

Point is fractional: symbol corners are found with sub-module precision, and rounding to integers would lose exactly that. Box is the candidate rectangle in the source frame; Quad gives its four corners more precisely than a rectangle can: on a conveyor the symbol is captured at an angle.

for (const dmr::Point& p : c.corners.corner)
    drawTo(p.x, p.y);
Corner order

Quad corners go around the symbol, and index 3 holds the one the detector took for the corner of the solid L-shaped finder — a decision made from edge density before any reading. The final symbol rotation is chosen later and may differ from it. corner[3] is fine for drawing overlays on the frame, but not for drawing conclusions about the symbol's content.

Settings

The defaults are not arbitrary — they are the result of testing on a large set of frames. Start with a preset rather than individual fields — the preset name already tells which scenario it is tuned for.

PresetPurposeHow it differs
Settings::stream()A frame stream of one label — conveyor, camera. The defaultSize hint from previous frames on, size search off
Settings::single()One-off images — a folder, manual checksNo hint; 24 symbol sizes are tried
Settings::multi(n)Multiple codes in one framemaxCodes = n (0 — as many as found); four times as expensive as a single read
auto s = dmr::Settings::stream();
s.budgetMs   = 120.0;     // 0 — no limit
s.requireGs1 = true;      // accept only GS1-structured codes
const dmr::Scanner scanner(s);
stream() vs single()

The difference is not stylistic but in speed. single() tries up to 24 symbol sizes when the size is not known in advance — on a frame that could not be read in the end (a failure) this is noticeably slower than without the search, although it raises the success rate by a few percent. In a frame stream of one label the size does not change from frame to frame, so the search gives no accuracy gain at all — it only costs time on every failure. That is why it is off in stream() and on in single(): for unrelated images, where you don't know the size in advance, the cost is justified.

All fields

FieldDefaultMeaning
maxCodes1How many codes to look for: 1 — one, 0 — as many as found, N — at most N
requireGs1falseAccept only GS1-structured codes. Successful Reed-Solomon correction alone does not prove a correct read — the check also looks at the content format (AI 01, a 14-digit GTIN with a valid check digit, then AI 21)
forcedRotation-1Symbol rotation, 0..3 — quarter turns clockwise; -1 — detect automatically
budgetMs0.0Time budget per frame, ms; 0 — no limit. The deadline is soft: it is checked between search stages, not inside them, so frame time may exceed the budget by the duration of the current stage
rowParalleltrueSplit rows of image kernels among pool threads while the frame runs on a single thread. Lowers frame latency at the cost of CPU time — turn it off if the machine runs several scanners or if throughput matters more than latency
maxSizeTries1How many symbol sizes to try when the size cannot be trusted; 1 — no search
warpScalesemptyRectification canvas scales for the last pass, pixels per module; empty — no search. Parsed from a string such as "7,8,10" by setWarpScales()
maxCandidates6How many candidates to take from the L-pattern detector per pass
readerCacheMb64.0Memory limit, MB per frame, for reusing work; 0 — store nothing. Does not affect the result
mcHintRun5How many consecutive frames must agree on one symbol size for it to be tried first from then on; 0 — no hint
maxPatternError0.25Maximum error fraction in the function modules (L-shaped finder, timing pattern) for a result to be considered at all
lpatRetryScale0.8Repeat the L-pattern search at a finer scale as a second hypothesis; 0 — no repeat
directSamplingfalseSample module intensities directly from the source, bypassing the rectification canvas
useLPatternFinder / useTextureFindertrue / trueCandidate sources; turning them off is meant for comparing detectors on the same corpus
collectDetailsfalseCollect a breakdown of the read or the failure into ScanResult::details (about 2 MB per result)

Diagnostics

The library does not write to the console or read environment variables — the log sink is passed explicitly, and without it log lines are not even built:

using LogSink = std::function<void(std::string_view line)>;

void setLogSink(LogSink sink);   // empty — turn off
void setVerbose(bool on);        // off by default
bool isVerbose();                // on AND a sink is set
std::ostream& dbg();             // output stream: to the sink, otherwise nowhere
dmr::setLogSink([](std::string_view line) { std::cerr << line; });
dmr::setVerbose(true);

The breakdown of why a particular read succeeded or failed is separate and on demand, as a string rather than in the log. It is collected only if Settings::collectDetails was on during the read:

struct ExplainOptions {
    int moduleCount     = 0;      // size given at read time; 0 — detected automatically
    int forcedRotation  = -1;     // rotation given at read time
    bool images         = false;  // save debug PNGs next to the working directory
    std::string imagePath;        // image name — for labeling debug files
};

std::string explain(const Details& details, const ExplainOptions& opt = {});
auto s = dmr::Settings::stream();
s.collectDetails = true;                 // about 2 MB per result
const dmr::ScanResult r = dmr::Scanner(s).scan(frame);
if (r.details) std::cerr << dmr::explain(*r.details);
explain() does not deliver a verdict

The frame status has already been set by the scanner. When a symbol is found but not read, there is nothing to break down — so the crop is rectified and decoded again to give the failure an explanation, which costs about as much as reading the frame itself.

Library version

#define DMR_VERSION_MAJOR 1
#define DMR_VERSION_MINOR 0
#define DMR_VERSION_PATCH 0
#define DMR_VERSION_STRING "1.0.0"

#define DMR_VERSION_AT(ma, mi, pa) ((ma) * 10000 + (mi) * 100 + (pa))
#define DMR_VERSION DMR_VERSION_AT(DMR_VERSION_MAJOR, DMR_VERSION_MINOR, DMR_VERSION_PATCH)

namespace dmr { std::string version(); }

The macros describe the header you compiled against; dmr::version() describes the library you linked against. They can diverge: the library is installed into a prefix and lives there longer than whoever built against it remembers.

#if DMR_VERSION >= DMR_VERSION_AT(1, 1, 0)
    // code for header 1.1.0 and newer
#endif

License

The scanner reads frames while the trial period (one month from the first run on the machine, no network or registration needed) or a license is in effect. Otherwise Scanner::scan() immediately returns Status::Unlicensed without processing the frame. The license is bound to the hardware (motherboard, CPU, system disk), not to the OS installation.

#include <dmr/License.h>

const dmr::LicenseStatus s = dmr::license::status();
if (!s.canScan()) show(s.message);   // "Пробный период закончился. Нужна лицензия" (trial over, license required)

dmr::LicenseResult r = dmr::license::activate(keyFromUser);   // "DMR-XXXXX-XXXXX-XXXXX-XXXXX"
if (!r.ok) show(r.message);

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

FieldMeaning
stateLicenseState
canScan()Whether the scanner 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
offlineUntilBeyond this time the license does not work without contacting the server; 0 — no limit
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

Activation: online and offline

Online — a key from your account; the machine contacts the license server itself:

dmr::LicenseResult r = dmr::license::activate("DMR-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:

dmr::license::writeActivationRequest("request.json");   // → upload to your account, get license.lic
dmr::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 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.

Deactivation and other functions

namespace dmr::license {
    void configure(const LicenseOptions& options);   // before the first network call; 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::string& receiptPath, const std::string& reason = {});
    LicenseResult writeActivationRequest(const std::string& path);
    LicenseResult importLicenseFile(const std::string& path);
    std::string machineFingerprint();                           // for support requests
}

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 network access, use deactivateToFile(): 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 (15000 ms by default).

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 also single-use — it carries the number from the latest server response received, so the server will not accept a receipt from a restored copy of the machine's old state. The server issues the secret for signing receipts only on the machine's first activation. For licenses with full autonomy (full) releasing a seat consumes the transfer limit; once it is exhausted, transfers are handled by support.

Activation key

The activation key for activate() is issued in your account after you purchase a plan in the catalog.

Command line

The package includes, alongside the library, a ready-made command-line tool dmr — for quick checks without writing code: a single image, a folder, a list of paths, or a stream for an external service.

dmr <image> [module_count] [--verbose] [--rot 0..3] [--budget-ms N] [--codes N]
dmr --batch  <list>  [module_count] [--verbose]
dmr --folder <directory> [module_count] [--verbose]
dmr --serve  [module_count] [--verbose]
dmr --license status | activate <key> | refresh | deactivate | fingerprint [--json]

Single image

The utility prints its messages in Russian: “запуск” is startup time, “обработка” is processing time, “код” is the decoded code.

$ dmr label.jpg
запуск: 4.6 мс
обработка: 24.9 мс
код: 0104627191145677215n5Gr,pHTGIU&<GS>93CuXU

Batch mode and streaming

--batch and --folder run one process over the whole set of images known in advance, with results line by line on stdout: path · exit code · milliseconds · text.

--serve uses the same line format, but the images are not known in advance: paths arrive one at a time, one line from stdin each, and are processed immediately — the process keeps the scanner alive between requests from an external service instead of paying for initialization on each one. The result is flushed immediately (std::flush) without waiting for the whole input to close — this is how the demo section of this site (/scan) runs one process for all requests instead of one process per image.

$ mkfifo queue
$ dmr --serve < queue &
$ echo /path/to/image.jpg > queue
/path/to/image.jpg	0	91.2	0104627191145677215n5Gr,pHTGIU&<GS>93CuXU

License: machine-readable output

All --license subcommands accept the --json flag — the same result as the human-readable output, but as a single JSON line on stdout, for scripts and admin panels:

$ dmr --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":"Пробный период: осталось 28 дн."}

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 not translated and its wording does not change. The message field is human-readable text 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 →