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.
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 program | What 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 program | Dynamic linking — C ABI, dmr_c |
| Python | The 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).
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-pkgconfInclude 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 dmrThe system libraries winhttp, crypt32, shell32 and advapi32 (used by the license check) are already listed in the package's Libs.private — no need to add them separately.
The package is a single static library, so the caller also needs the libraries it depends on. Without --static they will not be on the command line, and linking will fail with unresolved symbols.
In meson — a dependency by package name:
dmr_dep = dependency('dmr', required: true, static: true)
executable('myprogram', 'main.cpp', dependencies: [dmr_dep],
link_args: ['-static']) # Windows: MinGW runtime linked into the programOn Windows, -static also links the compiler runtime into the program: without it, the program will need libstdc++-6.dll, libgcc_s_seh-1.dll and libwinpthread-1.dll from MSYS2 next to it. On Linux it is not needed. A complete meson.build is in “Quick start”.
Dynamic linking — C ABI
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_cThis 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.defthen, 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.libAt 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 buildThe 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 buildFor a frame from your own capture pipeline (camera SDK, OpenCV buffer, Windows bitmap) the disk is not involved — you build a view over the buffer you already have, without copying:
const 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.
| Header | Contents |
|---|---|
dmr/Dmr.h | Umbrella header — includes everything below |
dmr/Image.h | ImageView, Image, PixelFormat, loadImage, paths |
dmr/Scanner.h | Scanner, ScanResult, Code, Status |
dmr/Settings.h | Scanner settings and presets |
dmr/Geometry.h | Point, Box, Quad — where the code is located in the frame |
dmr/Diagnostics.h | explain(), log sink (LogSink) |
dmr/License.h | License check and activation, namespace dmr::license |
dmr/Version.h | Header 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();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
| Field | Type | Meaning |
|---|---|---|
status | Status | How processing of the frame ended |
codes | std::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 |
timedOut | bool | The 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 |
elapsedMs | double | Frame processing time, ms, excluding loading the image from disk |
details | std::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
| Field | Type | Meaning |
|---|---|---|
text | std::string | The decoded string. Bytes as is: GS separators stay as the byte 0x1D and are not replaced with a printable form |
box | Box | Where the symbol was found in the source frame — the candidate rectangle |
corners | Quad | The four symbol corners in the same frame, more precise than the box (see “Geometry”) |
moduleCount | int | Symbol 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;
}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);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.
| Preset | Purpose | How it differs |
|---|---|---|
Settings::stream() | A frame stream of one label — conveyor, camera. The default | Size hint from previous frames on, size search off |
Settings::single() | One-off images — a folder, manual checks | No hint; 24 symbol sizes are tried |
Settings::multi(n) | Multiple codes in one frame | maxCodes = 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);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
| Field | Default | Meaning |
|---|---|---|
maxCodes | 1 | How many codes to look for: 1 — one, 0 — as many as found, N — at most N |
requireGs1 | false | Accept 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 | -1 | Symbol rotation, 0..3 — quarter turns clockwise; -1 — detect automatically |
budgetMs | 0.0 | Time 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 |
rowParallel | true | Split 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 |
maxSizeTries | 1 | How many symbol sizes to try when the size cannot be trusted; 1 — no search |
warpScales | empty | Rectification canvas scales for the last pass, pixels per module; empty — no search. Parsed from a string such as "7,8,10" by setWarpScales() |
maxCandidates | 6 | How many candidates to take from the L-pattern detector per pass |
readerCacheMb | 64.0 | Memory limit, MB per frame, for reusing work; 0 — store nothing. Does not affect the result |
mcHintRun | 5 | How many consecutive frames must agree on one symbol size for it to be tried first from then on; 0 — no hint |
maxPatternError | 0.25 | Maximum error fraction in the function modules (L-shaped finder, timing pattern) for a result to be considered at all |
lpatRetryScale | 0.8 | Repeat the L-pattern search at a finer scale as a second hypothesis; 0 — no repeat |
directSampling | false | Sample module intensities directly from the source, bypassing the rectification canvas |
useLPatternFinder / useTextureFinder | true / true | Candidate sources; turning them off is meant for comparing detectors on the same corpus |
collectDetails | false | Collect 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 nowheredmr::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);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
#endifLicense
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
| Value | Meaning |
|---|---|
Trial | Trial period in progress |
Active | License is in effect |
RefreshDue | In effect, but there has been no contact with the server for a long time; auto-check retries hourly |
TrialExpired | Trial period over, no license |
Expired | The paid license term has ended; a renewal in your account is picked up by the auto-check |
NetworkRequired | Cannot continue without contacting the server; as soon as the server responds, the auto-check restores the license |
WrongMachine | The license was issued to a different machine |
Invalid | The license record fails signature verification |
LicenseStatus
| Field | Meaning |
|---|---|
state | LicenseState |
canScan() | Whether the scanner reads right now — true for Trial, Active or RefreshDue |
validUntil | End of the trial period or of the paid term, Unix seconds UTC |
refreshAfter | When the token is considered stale and the license moves to RefreshDue; 0 — never goes stale |
offlineUntil | Beyond this time the license does not work without contacting the server; 0 — no limit |
daysLeft | Whole days until the nearest of the limits above; 0 — limit passed |
licenseId / activationId | IDs of the license and of the specific activation |
offlineMode | limited | extended | full; empty without a license |
clockRollback | The system clock is behind a time the machine has already seen — rolling back the clock does not extend the term |
message | The state in words, for display to the user |
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 oneA 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.
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>93CuXUBatch 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>93CuXULicense: 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 →