# libscanner — full API documentation > Generated from https://libscanner.com/en/docs. Index: https://libscanner.com/llms.txt # DataMatrix Reader 1.0 — C++ and C ABI > Reads GS1 DataMatrix codes from industrial camera frames — for conveyor lines marked under Chestny ZNAK, the Russian product marking system. Source page: https://libscanner.com/en/docs/datamatrix-gs1/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/`, plus a flat C interface for other languages (`dmr_c.h`, see C ABI). 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: **Windows:** ``` 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 ``` **Linux:** ``` 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 lib/libdmr_c.so C ABI library lib/pkgconfig/dmr_c.pc bin/dmr 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](https://libscanner.com/en/docs/datamatrix-gs1/1.0/python.md) | ### 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`, 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). **Windows:** > **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](https://www.msys2.org/) — run the installer from the website, then in its **MINGW64** shell (the package is built in exactly this environment, not UCRT64): > > ```bash > 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: ```bash $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. **Linux:** The package is built with GCC 12 (Debian 12): GCC 12 or newer is required. Include paths and the link line come from `dmr.pc`; point an environment variable at `lib/pkgconfig` of the unpacked package — the unpack directory can be anywhere: ```bash PKG_CONFIG_PATH=/path/to/unpacked/lib/pkgconfig pkg-config --static --cflags --libs dmr ``` `-pthread -ldl` are required — they are already in the package. The `.pc` does not add `-std=c++20`: pass it yourself (GCC 12 defaults to C++17). Without pkg-config the same line by hand: ```bash g++ -std=c++20 main.cpp -o myprogram -I/path/to/unpacked/include /path/to/unpacked/lib/libdmr.a -pthread -ldl ``` OpenSSL is not a link dependency: for the license server's `https://` the library loads `libssl.so.3` (or `.so.1.1`) at run time. > **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: ```cpp 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`. The rules of the interface, a complete example and every function — in C ABI. **Windows:** **MinGW-w64 GCC** — via `dmr_c.pc` and the import library `lib\dmr_c.dll.a`, without `--static`: ```bash $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`: ```cpp prefix=${pcfiledir}/../.. includedir=${prefix}/include libdir=${prefix}/lib Name: dmr_c Version: 1.0.13 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: ```bash 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: ```bash 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` goes next to `dmr_c.dll` or next to the `.exe` (see “License”). **Linux:** ```bash PKG_CONFIG_PATH=/path/to/unpacked/lib/pkgconfig pkg-config --cflags --libs dmr_c ``` This yields `-I…/include -L…/lib -ldmr_c`. At run time the loader must find `libdmr_c.so`: either via `LD_LIBRARY_PATH`, or put it next to the program and set the search path at link time: ```bash LD_LIBRARY_PATH=/path/to/unpacked/lib ./myprogram # or: libdmr_c.so next to the program cc main.c -o myprogram $(pkg-config --cflags --libs dmr_c) -Wl,-rpath,'$ORIGIN' cp /path/to/unpacked/lib/libdmr_c.so . ``` The key file `license.key` goes next to `libdmr_c.so` or next to the program (see “License”). The library has no versioned SONAME — the file is simply `libdmr_c.so`. In meson — likewise by package name, without `static`: ```cpp 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](https://libscanner.com/en/docs/datamatrix-gs1/1.0/python.md). ## Quick start A minimal program: load an image from disk, read it, print the text. All you need is one header: ```cpp #include #include 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.13-windows-x64` and a `meson.build` file: ```cpp 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.13-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.13-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: ```bash 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: ```bash meson setup build --pkg-config-path="$PWD/dmr-1.0.13-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: ```cpp 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 C++ headers, all under `include/dmr/`; the eighth, `dmr/Dmr.h`, includes them all at once. The ninth file, `dmr/dmr_c.h`, is the separate C interface (C ABI). | 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 `` and ``, 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()`. ```cpp 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; // An image from disk and the codes in it under ONE deadline (budgetMs, // timeoutMs): loading and scanning together. Not loaded — ImageTooLarge // or InvalidImage (plus timedOut if decoding hit the deadline); details // in load. Since 1.0.10. ScanResult scanFile(const std::string& path, int moduleCount = 0, LoadStatus* load = nullptr) 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: ```cpp scanner.resetStream(); ``` > **Errors are returned, not thrown.** `scan()` catches everything inside: an internal failure (for example, out of memory) is `Status::Failed`, the process stays alive. `loadImage()` reports problems with `LoadStatus`. Only `std::bad_alloc` can leave the `Scanner` constructor and the `license::` functions — like any C++ code that allocates memory. The C ABI does not let even that through. > **Move-only.** `Scanner` can be moved (`Scanner&&`), but not copied — the copy constructor and copy assignment operator are explicitly deleted. ## Scan result ### Status ```cpp 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 ImageTooLarge = 4, // frame exceeds Settings::imageLimits, not processed InvalidImage = 5, // inconsistent frame description: stride smaller than // the width, unknown pixel format Failed = 6, // processing aborted by an internal error (e.g. out of // memory); the process stays alive }; ``` With `Status::Unlicensed` the frame was not processed at all — the reason is given by `dmr::license::status()` (see “License”). `Ok` always comes with at least one code in `codes`; with any other status `codes` is empty. ### ScanResult | Field | Type | Meaning | | --- | --- | --- | | `status` | `Status` | How processing of the frame ended | | `codes` | `std::vector` | 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` or `Settings::timeoutMs`. 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` | 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 | `codes` are already de-duplicated: two candidates with the same string and overlapping `box` are one symbol seen twice and come back as one code; two identical labels whose boxes do not overlap stay two codes. Order — top to bottom, then left to right. ### Multiple codes per frame ```cpp 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); ``` `multi(n)` is `stream()` with `maxCodes = n`: tuned for a camera stream. For unrelated images (a folder, single files) with several codes, combine it with what `single()` changes — fields are independent: ```cpp auto s = dmr::Settings::multi(0); s.mcHintRun = 0; // no hint from previous frames s.maxSizeTries = 24; // try all sizes on a frame where nothing was read ``` ## Frame ### PixelFormat ```cpp 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. ```cpp 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: ```cpp 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()`: ```cpp 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 ```cpp enum class LoadStatus { Ok, // image read NotFound, // no such file Unreadable, // the file exists but is not a readable image TooLarge, // size from the header exceeds ImageLimits; pixels were not read }; 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, const ImageLimits& limits = ImageLimits{}); // 16384 per side, 100 MP ``` Formats: **JPEG and PNG** — recognised by the first bytes of the file, not by the extension. Baseline and progressive JPEG, grayscale or color; CMYK JPEG is not supported. PNG of any bit depth and color type: 16-bit is reduced to 8, palette and low bit depths are expanded, the alpha channel is dropped (no compositing over a background). BMP, GIF, TIFF and WebP give `Unreadable` — convert them first or pass the decoded pixels as an `ImageView`. A truncated JPEG is read as far as it goes. 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. ```cpp 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 ```cpp 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. ```cpp 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. | 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 | `stream()` with `maxCodes = n` (0 — as many as found): hint on, no size search. Every frame goes through the full search — about four times as expensive as a single read | ```cpp 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 | 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 | | `imageLimits` | 16384 per side, 100 000 000 px | The largest frame the scanner agrees to process (`dmr::ImageLimits` from `dmr/Image.h`: `maxSide`, `maxPixels`; 0 — the hard ceiling 65535 and 2^29). A larger frame gets `Status::ImageTooLarge` at once. Pass the same limits to `loadImage()` — there they are checked against the file header before memory is allocated. A protection, not a quality setting | | `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 | | `timeoutMs` | 10000.0 | Safety timeout per frame, ms; 0 and anything above 60000 mean the 60 s ceiling: a frame is never processed longer. For frames from an untrusted source also set `budgetMs`. A protection against hostile frames (hatching, texture edge to edge), not a latency setting: works like `budgetMs` — the smaller of the two applies, on expiry `timedOut` is set — but does not change how the search trades CPU for latency. The C++ `loadImage()` takes only size limits, while `Scanner::scanFile()` loads and scans under one deadline; in the C ABI (`dmr_load_image_ex` → `DMR_LOAD_TIMED_OUT`) and in Python (`dmr.Image(path, settings=…)`) the deadline applies to decoding too | | `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; values outside 1…32 are skipped. 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 — the log sink is passed explicitly, and without it log lines are not even built (from the environment it reads only the message language, see below): ```cpp using LogSink = std::function; 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 ``` ```cpp 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: ```cpp 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 = {}); ``` ```cpp 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. ### Message language Human-readable text — the license state (`status().message`), error messages, the debug log — is in Russian or English. By default the language comes from the `LIBSCANNER_LANG` environment variable (`ru` or `en`); without it, from the system language: a Russian system gives Russian, any other (including the `C` locale in a container) gives English. Machine-readable fields and codes are not translated. ```cpp bool setMessageLanguage(std::string_view lang); // "ru" or "en"; otherwise false std::string_view messageLanguage(); // "ru" or "en" ``` ```cpp dmr::setMessageLanguage("en"); // for the whole process ``` ## Library version ```cpp #define DMR_VERSION_MAJOR 1 #define DMR_VERSION_MINOR 0 #define DMR_VERSION_PATCH 13 #define DMR_VERSION_STRING "1.0.13" #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. ```cpp #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. ```cpp #include const dmr::LicenseStatus s = dmr::license::status(); if (!s.canScan()) show(s.message); // "The trial period has ended. A license is required" dmr::LicenseResult r = dmr::license::activate(keyFromUser); // "DMR-XXXXX-XXXXX-XXXXX-XXXXX" if (!r.ok) show(r.message); ``` ### Key file license.key Instead of calling `activate()` from your program, put the key into a text file `license.key`. The library looks for it, first match wins: 1. next to the module that contains the library: `dmr_c.dll` / `libdmr_c.so` for the C ABI and Python (`dmr/_lib/`), the program itself for static `libdmr.a`; 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 — `DMR-XXXXX-XXXXX-XXXXX-XXXXX` (case, spaces and dashes do not matter; a UTF-8 BOM is fine; files over 4 KB are ignored). The file is read when the library starts (the first `Scanner` or `license::` call — this call waits for the server once) and again by the background check about every hour, so a key 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. If the key is rejected, `status().message` says so. ### Where the trial and license state is kept Windows: `%ProgramData%\dmr\license.dat`, `%LOCALAPPDATA%\dmr\license.dat` and the registry value `HKCU\Software\dmr`. Linux: `/var/lib/dmr/license.dat` (writable only by root) and `~/.config/dmr/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. ### 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 — in the message language (below) | ### Activation: online and offline Online — a key from your account; the machine contacts the license server itself: ```cpp 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`: ```cpp 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 ```cpp 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. A seat of a license can be released by yourself at most twice in 30 days in the limited mode and at most once in the extended mode; further 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](https://libscanner.com/en). ## C ABI The same library behind a flat C99 interface — header `dmr/dmr_c.h`, library`dmr_c.dll` / `libdmr_c.so` (how to link it —Installation). Use it from C, from MSVC and from any language that can call C functions (C#, Go, Delphi, Rust…). The rules of the boundary: - Only plain C types cross it: integers, `double`, pointers to buffers of known size, enumerations 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 has its own `*_free` function (`dmr_image_free`,`dmr_scan_result_free`, `dmr_free_string`; scanner — `dmr_scanner_destroy`). Pointers from accessors (`dmr_scan_result_code_text` and the like) live as long as the object they came from and are not freed. - `dmr_settings_t` starts with `struct_size` and the `abi_tag` marker: the caller sets both **before** applying a preset — in C simply `dmr_settings_t s = DMR_SETTINGS_INIT;`, in C++ `dmr_settings_prepare(&s)`. A preset writes no further than `struct_size`; fields the caller does not know (a newer library) keep the defaults. A foreign marker or an out-of-range size is rejected: the preset returns 0, `dmr_scanner_create` returns `NULL`. A program built with an ABI 1.x or 2.0 header (dmr before 1.0.8) must be rebuilt. - The decoded text keeps the GS separator as the byte `0x1D` (unlike the`dmr` tool, which prints ``) and may contain a zero byte — take the length from `dmr_scan_result_code_text_len`, not `strlen()`. - Paths are in the process's system code page, like `argv` (on Windows this is not UTF-8 unless the process code page is set to UTF-8). - Status values are the same as `dmr::Status` (see “Scan result”), plus`DMR_STATUS_INTERNAL_ERROR = -1` for a failure of the C layer itself.`dmr_scanner_scan` returns `NULL` only on an internal error; “no code” is a result with `DMR_STATUS_NOT_FOUND`. - License functions return the same JSON as `dmr --license … --json` (see “Command line”), one contract for the shell, the C ABI and Python. ### Complete example: read every code from a file ```cpp #include #include /* Read every code from an image file: 0 — read, 1 — none, 2 — error. */ int main(int argc, char** argv) { if (argc < 2) return 2; dmr_settings_t settings = DMR_SETTINGS_INIT; /* size and marker — before the preset */ dmr_settings_multi(&settings, 0); /* all codes in the image */ settings.mc_hint_run = 0; /* a single file: no hint from previous frames, */ settings.max_size_tries = 24; /* all sizes — like single() */ dmr_image_t* image = NULL; if (dmr_load_image_ex(argv[1], DMR_LOAD_GRAY, &settings, &image) != DMR_LOAD_OK) return 2; /* no such file, not an image, too large */ dmr_scanner_t* scanner = dmr_scanner_create(&settings); if (!scanner) { dmr_image_free(image); return 2; } dmr_image_view_t view = dmr_image_view(image); dmr_scan_result_t* r = dmr_scanner_scan(scanner, &view, 0); /* 0 — size auto */ int rc = 2; if (r) { /* NULL — internal error */ dmr_status_t st = dmr_scan_result_status(r); if (st == DMR_STATUS_OK) { for (int i = 0; i < dmr_scan_result_code_count(r); ++i) { /* the text may contain 0x1D (GS) and even 0x00 — use the length, not strlen() */ fwrite(dmr_scan_result_code_text(r, i), 1, dmr_scan_result_code_text_len(r, i), stdout); putchar('\n'); } rc = 0; } else if (st == DMR_STATUS_NOT_FOUND || st == DMR_STATUS_NOT_DECODED) { rc = 1; } else if (st == DMR_STATUS_UNLICENSED) { char* why = dmr_license_status_json(); /* {"state":"trial_expired",...} */ fprintf(stderr, "%s\n", why ? why : "unlicensed"); dmr_free_string(why); } dmr_scan_result_free(r); } dmr_scanner_destroy(scanner); dmr_image_free(image); return rc; } ``` On Linux: `cc main.c -o read $(pkg-config --cflags --libs dmr_c)`, run with`LD_LIBRARY_PATH=/path/to/unpacked/lib ./read label.png`. On a code from a Chestny ZNAK pack it prints the string with raw `0x1D` bytes between the elements. ### Reference Every declaration of `dmr_c.h` (ABI 3.1, library 1.0.13); comments shortened — the full ones are in the header itself. ```cpp /* ── versions ── */ #define DMR_C_ABI_VERSION_MAJOR 3 #define DMR_C_ABI_VERSION_MINOR 0 size_t dmr_c_abi_version(char* buf, size_t buf_size); /* "3.1" */ size_t dmr_version(char* buf, size_t buf_size); /* library version, "1.0.13" */ /* ── frame ── */ typedef enum { DMR_PIXEL_GRAY8 = 0, DMR_PIXEL_BGR8 = 1, DMR_PIXEL_RGB8 = 2, DMR_PIXEL_BGRA8 = 3, DMR_PIXEL_RGBA8 = 4 } dmr_pixel_format_t; typedef struct { const uint8_t* data; /* caller-owned, alive for the whole scan call */ int width; int height; int stride; /* bytes per row; 0 — tightly packed */ dmr_pixel_format_t format; } dmr_image_view_t; typedef struct dmr_image dmr_image_t; /* opaque, library-owned */ typedef struct dmr_settings dmr_settings_t; /* defined below */ typedef enum { DMR_LOAD_COLOR = 0, DMR_LOAD_GRAY = 1 } dmr_load_as_t; typedef enum { DMR_LOAD_OK = 0, DMR_LOAD_NOT_FOUND = 1, DMR_LOAD_UNREADABLE = 2, DMR_LOAD_TOO_LARGE = 3, DMR_LOAD_TIMED_OUT = 4, DMR_LOAD_INVALID_ARGUMENT = 5 } dmr_load_status_t; dmr_load_status_t dmr_load_image(const char* path, dmr_load_as_t as, dmr_image_t** out); dmr_load_status_t dmr_load_image_ex(const char* path, dmr_load_as_t as, const dmr_settings_t* settings, /* NULL — defaults */ dmr_image_t** out); dmr_image_view_t dmr_image_view(const dmr_image_t* image); void dmr_image_free(dmr_image_t* image); /* ── settings: same fields and defaults as dmr::Settings ── */ struct dmr_settings { uint32_t struct_size; /* sizeof(dmr_settings_t), set by the caller FIRST */ uint32_t abi_tag; /* DMR_SETTINGS_TAG, also before the preset */ int max_codes; /* 1 — one, 0 — all, N — at most N */ int require_gs1; /* 0/1 */ int forced_rotation; /* -1 — auto */ double budget_ms; /* 0 — no limit */ int row_parallel; /* 0/1 */ int max_size_tries; int warp_scales[16]; /* DMR_MAX_WARP_SCALES */ int warp_scales_count; /* values outside 1..32 are skipped */ int max_candidates; double reader_cache_mb; int mc_hint_run; double max_pattern_error; double lpat_retry_scale; int direct_sampling; /* 0/1 */ int use_lpattern_finder; /* 0/1 */ int use_texture_finder; /* 0/1 */ int collect_details; /* 0/1 */ int max_image_side; /* 0 — library ceiling 65535; presets: 16384 */ int64_t max_image_pixels; /* 0 — ceiling 2^29; presets: 100 000 000 */ double timeout_ms; /* safety timeout, presets: 10000; 0 — 60 s ceiling */ }; #define DMR_SETTINGS_TAG 0x33524D44u /* 'D' 'M' 'R' '3' */ #define DMR_SETTINGS_INIT { .struct_size = (uint32_t)sizeof(dmr_settings_t), .abi_tag = DMR_SETTINGS_TAG } #define DMR_SETTINGS_MAX_SIZE 4096u static inline void dmr_settings_prepare(dmr_settings_t* s); /* zeroes, sets size and marker */ int dmr_settings_stream(dmr_settings_t* out); /* 1 — filled; 0 — NULL, foreign */ int dmr_settings_single(dmr_settings_t* out); /* marker or size */ int dmr_settings_multi(dmr_settings_t* out, int max_codes); /* ── scanner ── */ typedef struct dmr_scanner dmr_scanner_t; /* opaque */ dmr_scanner_t* dmr_scanner_create(const dmr_settings_t* settings); /* NULL settings — stream() */ void dmr_scanner_destroy(dmr_scanner_t* scanner); int dmr_scanner_module_count_hint(const dmr_scanner_t* scanner); void dmr_scanner_reset_stream(dmr_scanner_t* scanner); /* ── result ── */ typedef enum { DMR_STATUS_OK = 0, DMR_STATUS_NOT_FOUND = 1, DMR_STATUS_NOT_DECODED = 2, DMR_STATUS_UNLICENSED = 3, DMR_STATUS_IMAGE_TOO_LARGE = 4, DMR_STATUS_INVALID = 5, DMR_STATUS_FAILED = 6, DMR_STATUS_INTERNAL_ERROR = -1 } dmr_status_t; typedef struct dmr_scan_result dmr_scan_result_t; /* opaque */ dmr_scan_result_t* dmr_scanner_scan(dmr_scanner_t* scanner, const dmr_image_view_t* frame, int module_count); /* 0 — auto; NULL — internal error */ /* ABI 3.1: an image from disk and the codes in it under the scanner's single * deadline; the load outcome goes to *load_status (NULL — not needed) */ dmr_scan_result_t* dmr_scanner_scan_file(dmr_scanner_t* scanner, const char* path, int module_count, dmr_load_status_t* load_status); void dmr_scan_result_free(dmr_scan_result_t* result); dmr_status_t dmr_scan_result_status(const dmr_scan_result_t* result); int dmr_scan_result_timed_out(const dmr_scan_result_t* result); double dmr_scan_result_elapsed_ms(const dmr_scan_result_t* result); int dmr_scan_result_code_count(const dmr_scan_result_t* result); const char* dmr_scan_result_code_text(const dmr_scan_result_t* result, int index); size_t dmr_scan_result_code_text_len(const dmr_scan_result_t* result, int index); void dmr_scan_result_code_box(const dmr_scan_result_t* result, int index, int* x, int* y, int* width, int* height); void dmr_scan_result_code_corners(const dmr_scan_result_t* result, int index, float corners_xy[8]); /* x0,y0 … x3,y3 */ int dmr_scan_result_code_module_count(const dmr_scan_result_t* result, int index); /* ── diagnostics ── */ typedef void (*dmr_log_sink_t)(const char* line, size_t len, void* user); /* line is not 0-terminated */ void dmr_set_log_sink(dmr_log_sink_t sink, void* user); void dmr_set_verbose(int on); int dmr_is_verbose(void); /* ── license: JSON as in "dmr --license … --json"; free with dmr_free_string, NULL — internal error ── */ void dmr_license_configure(const char* server_url, const char* app_version, int timeout_ms, int auto_check); char* dmr_license_status_json(void); char* dmr_license_activate_json(const char* key); char* dmr_license_refresh_json(void); char* dmr_license_deactivate_json(const char* reason); /* reason may be NULL */ char* dmr_license_deactivate_to_file_json(const char* path, const char* reason); char* dmr_license_write_activation_request_json(const char* path); char* dmr_license_import_license_file_json(const char* path); char* dmr_license_fingerprint_json(void); void dmr_free_string(char* s); ``` ## 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 [module_count] [--verbose] [--rot 0..3] [--budget-ms N] [--codes N] dmr --batch [module_count] [--verbose] dmr --folder [module_count] [--verbose] dmr --serve [module_count] [--verbose] dmr --license status | activate | refresh | deactivate | fingerprint [--json] ``` ### Single image The utility prints messages in the system language (English on any non-Russian system); `--lang ru|en` or the `LIBSCANNER_LANG` variable sets it explicitly (message language). The `--batch`/`--serve` lines and the `--json` output do not depend on the language. ``` $ dmr label.jpg startup: 4.6 ms processing: 24.9 ms code: 0104627191145677215n5Gr,pHTGIU&93CuXU ``` One line `code:` per code; in the text GS is shown as ``, TAB as ``, line breaks as spaces. When the budget cut the search short, a line `budget: exhausted` follows. Errors go to stderr. The exit code is the result: | Code | Meaning | | --- | --- | | 0 | Code read (also when the budget cut the search short but codes were found) | | 1 | No code found; also no such file, not a JPEG/PNG, decoding deadline exceeded | | 2 | Symbol found but not read | | 3 | No trial period and no license | | 4 | Image larger than the limit | | 5 | Invalid image description | | 6 | Internal error | ### 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`, separated by TAB — one line per code, one line with empty text for an image without a code; the second column uses the codes from the table above.`--corners` adds a fifth column, `x0,y0 x1,y1 x2,y2 x3,y3`. A summary goes to stderr. Without a license the run stops with exit code 3. `--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](https://libscanner.com/en/scan)) runs one process for all requests instead of one process per image. After the lines of each image comes a line `#END#`, so the reader knows the answer is complete even with several codes. The process exits on the end of input. ``` $ mkfifo queue $ dmr --serve < queue & $ echo /path/to/image.jpg > queue /path/to/image.jpg 0 91.2 0104627191145677215n5Gr,pHTGIU&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":"Trial period: 28 days left"} ``` 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 the message language. --- # DataMatrix Reader 1.0 — Python > Reads GS1 DataMatrix codes from industrial camera frames — for conveyor lines marked under Chestny ZNAK, the Russian product marking system. Source page: https://libscanner.com/en/docs/datamatrix-gs1/1.0?lang=python ## Overview The `dmr` package reads **DataMatrix (ECC200)** codes from production camera frames — for example, boxes labeled for Chestny ZNAK (Russia's national product marking system), where the symbol spans about a hundred pixels in the frame. It is a `ctypes` wrapper over the flat C ABI (`dmr_c`) of the same library described in the main documentation — a direct in-process call, with no overhead of launching an external process for every frame. The package has no external dependencies — only the standard library (`ctypes`). The library itself (`dmr_c.dll` on Windows, `libdmr_c.so` on Linux) ships prebuilt inside the wheel — there is nothing to build; the package just calls it. > **When the library is not available.** If there is no wheel for your platform (for example, Linux with glibc older than 2.36), there is a second option — run the prebuilt `dmr` utility (also part of the distribution) as an external process (`subprocess`), without this package at all. It is slower (process overhead) but always available — see "Via subprocess" below. ## Installation ```bash pip install libscanner-dmr ``` The PyPI package is called `libscanner-dmr` and is imported as `dmr`. pip picks the wheel for your platform: Windows x86-64, Linux x86-64, ARM64 and ARMv7 (Raspberry Pi OS 64- and 32-bit). The Linux wheels need glibc 2.36 or newer (Debian 12, Ubuntu 23.04+, Raspberry Pi OS Bookworm). The package also installs the `dmr` command (and `python -m dmr`). Python 3.9 or newer. Without access to PyPI, use the same wheel from the Downloads section of your account: **Windows:** ```bash pip install .\libscanner_dmr-1.0.13-py3-none-win_amd64.whl ``` **Linux:** ```bash pip install ./libscanner_dmr-1.0.13-py3-none-manylinux_2_36_x86_64.whl # x86-64 pip install ./libscanner_dmr-1.0.13-py3-none-manylinux_2_36_aarch64.whl # ARM64 pip install ./libscanner_dmr-1.0.13-py3-none-manylinux_2_36_armv7l.whl # ARMv7 ``` The library lives inside the package, in `dmr/_lib/`, and is loaded only from there, by full path; there is no system search. The `DMR_C_LIBRARY` variable (full path to the file) is only needed for a package built from source, which has no `_lib`. ## Quick start A minimal program: an image from disk, a scan, the text printed: ```python import dmr img = dmr.Image("frame.jpg", dmr.LoadAs.GRAY) # the scanner does not need color scanner = dmr.Scanner() # frame stream, the default result = scanner.scan(img.view()) if result.ok: print(result.text.decode("utf-8")) elif result.status == dmr.Status.NOT_FOUND: print("no symbol in the frame") else: print("symbol found but could not be read") ``` A frame from someone else's buffer — no copy, just a view of it (the buffer must stay alive for the duration of the `scan()` call): ```python view = dmr.ImageView(buf, width, height, stride, dmr.PixelFormat.BGR8) result = scanner.scan(view) ``` > **Create the scanner once, not per frame.** The symbol size hint (`Settings.mc_hint_run`) accumulates across frames and must outlive them — create one `Scanner` per frame stream. ## Modules Public names are exported directly from the `dmr` package — there is no need to import from submodules, except `dmr.license` and `dmr.diagnostics`, which have their own namespaces: | Name | What it is | | --- | --- | | `dmr.Image` / `dmr.ImageView` | A frame — owning (loaded from disk) or a view of someone else's buffer | | `dmr.PixelFormat` / `dmr.LoadAs` / `dmr.LoadStatus` | Pixel format, load mode and load status from disk | | `dmr.Scanner` / `dmr.scan` | The scanner object (frame stream) and a one-off free function | | `dmr.ScanResult` / `dmr.Code` / `dmr.Status` | The result of scanning a frame and of a single code | | `dmr.Box` / `dmr.Point` | Geometry of the found symbol — fields of `Code` | | `dmr.Settings` | Scanner settings and presets | | `dmr.DmrError` | C ABI boundary exception — not a failed scan, but a failure of the library itself | | `dmr.license` | License submodule — `import dmr.license as license` | | `dmr.diagnostics` | Logging submodule — `import dmr.diagnostics as diagnostics` | | `dmr.version()` / `dmr.abi_version()` | The version of the built library and the version of the C ABI boundary, separately | ## Scanner `Scanner` manages a C ABI resource — release it explicitly with `close()` or use it as a context manager (`with`); `__del__` is a safety net, but the finalization order at process exit is not always guaranteed. ```python class Scanner: def __init__(self, settings: Settings | None = None): ... def scan(self, frame: ImageView, module_count: int = 0) -> ScanResult: """Read the codes in a frame. module_count is the symbol side in modules, if known in advance; 0 means detect it.""" def scan_file(self, path, module_count: int = 0) -> ScanResult: """An image from disk and the codes in it under ONE deadline (budget_ms, timeout_ms). Not loaded — status IMAGE_TOO_LARGE or INVALID, details in load_status. Since 1.0.10.""" @property def module_count_hint(self) -> int: """Size suggested by previous frames; 0 means no hint.""" def reset_stream(self) -> None: """Forget the hint — the label has changed and you know it.""" def close(self) -> None: ... def __enter__(self) -> "Scanner": ... def __exit__(self, *exc) -> None: ... def scan(frame: ImageView, settings: Settings | None = None) -> ScanResult: """One-off scan without an object — the size hint from the frame stream does not work here: there is nothing to accumulate it in.""" def scan_file(path, settings: Settings | None = None) -> ScanResult: """One-off scan of an image from disk: loading and scanning under one deadline.""" ``` When the label on the conveyor changes, reset the hint explicitly: ```python scanner.reset_stream() ``` Several codes in one frame: ```python scanner = dmr.Scanner(dmr.Settings.multi(0)) # 0 — as many as found for code in scanner.scan(view).codes: print(code.text, code.box) ``` > **Context manager.** `with dmr.Scanner() as scanner: ...` — `close()` is called even if an exception is raised inside the block. ## Scan result ### Status ```python class Status(enum.IntEnum): OK = 0 # at least one code was read NOT_FOUND = 1 # no symbol found in the frame NOT_DECODED = 2 # symbol found but not read UNLICENSED = 3 # frame not processed: no trial period and no license IMAGE_TOO_LARGE = 4 # frame exceeds Settings.max_image_side / max_image_pixels INVALID = 5 # inconsistent frame description (stride, format) FAILED = 6 # processing aborted by an internal error ``` `OK` always comes with at least one code; with any other status `codes` is empty. Duplicate sightings of one symbol are already merged (same text and overlapping boxes); identical labels in different places stay separate codes. ### ScanResult ```python @dataclass(frozen=True) class ScanResult: status: Status codes: list[Code] timed_out: bool # the search was cut off by Settings.budget_ms or timeout_ms elapsed_ms: float load_status: LoadStatus | None = None # load outcome of scan_file(); None for scan() @property def ok(self) -> bool: ... # status == Status.OK @property def text(self) -> bytes: ... # text of the first code, or b"" ``` `codes` is empty if `status` is not `Status.OK`; codes are in reading order — top to bottom, and left to right when the top edges are equal. ### Code ```python @dataclass(frozen=True) class Code: text: bytes # NOT str — the GS1 separator stays as byte 0x1D box: Box # x, y, width, height — the candidate's rectangle in the frame corners: tuple # four Points in order around the symbol, more precise than the box module_count: int # symbol side in modules class Box(NamedTuple): x: int y: int width: int height: int class Point(NamedTuple): x: float y: float ``` > **Bytes, not a string.** `Code.text` is `bytes`: the GS1 separator stays as byte `0x1D`, not replaced with a printable form. Decode it yourself in whatever way fits your format: > > ```python > text = result.codes[0].text.decode("utf-8") > ``` `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 do not. The `corners` are more precise than `box`: on a conveyor the symbol is captured at an angle. ## Frame ### PixelFormat ```python class PixelFormat(enum.IntEnum): GRAY8 = 0 # one channel: brightness; the cheapest for the scanner BGR8 = 1 # three channels, OpenCV order RGB8 = 2 # three channels, the order used by most others BGRA8 = 3 # four channels, OpenCV and Windows order RGBA8 = 4 # four channels ``` The scanner works with brightness 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 of someone else's buffer — the library does not copy it, free it or keep it beyond the call; the buffer must stay alive for the whole call. Any object supporting the buffer protocol works (`bytearray`, a numpy array, `mmap`): ```python view = dmr.ImageView(buf, width, height, stride, dmr.PixelFormat.BGR8) print(view.width, view.height, view.stride, view.format) ``` For **non-writable** buffers (`bytes`, a read-only `memoryview`) the package makes one explicit copy — nothing like that happens silently inside the library. ### Image A frame owned by the library itself — loaded from disk. It is released on `close()`, on leaving a `with` block, or by garbage collection; an explicit `close()` is more reliable — `__del__` is not guaranteed to run at any particular time: ```python class LoadAs(enum.IntEnum): COLOR = 0 # three channels, PixelFormat.BGR8 GRAY = 1 # one channel, PixelFormat.GRAY8 class LoadStatus(enum.IntEnum): OK = 0 NOT_FOUND = 1 UNREADABLE = 2 TOO_LARGE = 3 # larger than Settings.max_image_side / max_image_pixels TIMED_OUT = 4 # decoding did not finish within budget_ms / timeout_ms INVALID_ARGUMENT = 5 # settings rejected by the C ABI boundary class Image: def __init__(self, path, as_: LoadAs = LoadAs.GRAY, settings: Settings | None = None): ... with dmr.Image("frame.jpg", dmr.LoadAs.GRAY) as img: if not img: print("status:", img.status.name) # NOT_FOUND, UNREADABLE, TOO_LARGE… else: result = scanner.scan(img.view()) ``` A failed load does not raise: the object is falsy and `status` says why; `view()` on such an object raises `ValueError`. `settings` passes the frame size limit and the deadline to the decoder (`None` — library defaults). Formats: **JPEG and PNG**, detected by content, not extension; CMYK JPEG is not supported; 16-bit, palette and alpha PNG are converted. BMP, GIF, TIFF and WebP give `UNREADABLE` — decode them yourself (for example, with Pillow) and pass the pixels as a `dmr.ImageView`. > **Path encoding on Windows.** File paths are encoded in the process's system code page (like `argv`), not in UTF-8 — on Windows this is usually an ANSI code page (`cp1251` on a Russian locale). The package does this conversion itself; you only need to know about it if you pass the path as ready-made `bytes` rather than a Python string. ## Settings Presets come from the library itself rather than being duplicated in Python — the defaults behind them are not arbitrary but the result of testing on a large set of frames: ```python settings = dmr.Settings.stream() # stream of frames of one label, the default settings = dmr.Settings.single() # one-off images: a folder, manual checks settings = dmr.Settings.multi(0) # several codes per frame, 0 — as many as found settings.budget_ms = 120.0 # set a field like in a regular dataclass scanner = dmr.Scanner(settings) ``` > **stream() vs single().** The difference is not a matter of style but of speed. `single()` tries up to 24 symbol sizes when the size is not known in advance — on a frame that ultimately cannot be read (a failed frame) this is noticeably slower than without the search, although it raises the share of successful reads by a few percent. On a stream of frames 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 scattered images where the size is not known in advance, the cost is justified. ### Settings fields | Field | Default | Meaning | | --- | --- | --- | | `max_codes` | 1 | How many codes to look for: 1 — one, 0 — as many as found, N — at most N | | `require_gs1` | False | Accept only codes with GS1 structure (AI 01, a 14-digit GTIN, then AI 21) | | `forced_rotation` | -1 | Symbol rotation, 0..3 — quarter turns clockwise; -1 — detect it | | `budget_ms` | 0.0 | Time budget per frame, ms; 0 — unlimited. The deadline is soft | | `timeout_ms` | 10000.0 | Safety timeout per frame, ms; 0 and anything above 60000 — the 60 s ceiling. Protection against hostile frames; the smaller of it and `budget_ms` applies | | `max_image_side` / `max_image_pixels` | 16384 / 100000000 | The largest frame to process; larger — `IMAGE_TOO_LARGE` without processing (and `TOO_LARGE` when loading with these settings) | | `row_parallel` | True | Split frame rows among pool threads while frames are processed one at a time | | `max_size_tries` | 1 | How many symbol sizes to try; 1 — no search | | `warp_scales` | [] | Rectification canvas scales for the last pass, 1…32 pixels per module (others raise ValueError); empty — no search | | `max_candidates` | 6 | How many candidates to take from the detector per pass | | `reader_cache_mb` | 64.0 | Memory limit, MB per frame, for reusing work; 0 — keep nothing | | `mc_hint_run` | 5 | How many consecutive frames must agree on one size before it is tried first | | `max_pattern_error` | 0.25 | Maximum share of errors in the finder/timing modules for a result to be considered at all | | `lpat_retry_scale` | 0.8 | Repeat the L-pattern search at a finer scale; 0 — do not repeat | | `direct_sampling` | False | Sample module brightness directly from the source image, bypassing the rectification canvas | | `use_lpattern_finder` / `use_texture_finder` | True / True | Candidate sources; turning them off is meant for comparing detectors on the same image set | > **collect_details is not exposed.** In this version the C ABI does not return the failure analysis (`explain()`) in structured form — so the field is not exposed in Python: there is no point offering a setting that cannot change anything. From Python, the analysis is only available through the `dmr` utility (see "Via subprocess" below). ## Diagnostics The library never writes to the console on its own — the log line sink is passed explicitly, and without it no lines are collected: ```python import dmr.diagnostics as diagnostics diagnostics.set_log_sink(lambda line: print(line.decode("utf-8"), end="")) diagnostics.set_verbose(True) print(diagnostics.is_verbose()) # True — enabled AND a sink is set ``` The `sink` receives the raw `bytes` of one line (library messages are in UTF-8) — whether to decode them is up to the caller. `set_log_sink(None)` turns the sink off. ## Library version ```python print(dmr.version()) # version of the BUILT dmr_c library print(dmr.abi_version()) # version of the C ABI boundary itself, separate from the algorithm version ``` ## License The scanner reads frames while a trial period or a license is in effect. A failed operation (wrong key, network unavailable) is not an exception but a regular `dict` with a clear reason; an exception is raised only if something is wrong with the C ABI boundary: ```python import dmr.license as license s = license.status() if not s["can_scan"]: print(s["message"]) # "The trial period has ended. A license is required" r = license.activate("DMR-XXXXX-XXXXX-XXXXX-XXXXX") if not r["ok"]: print(r["error"], r["message"]) ``` The dictionaries have the same JSON format as `dmr --license ... --json`: one contract for the shell, the C ABI and the package. **Key file.** Instead of calling `activate()`, put the key into a text file `license.key` in `dmr/_lib/` of the installed package (next to the library; the directory of `python` itself is the fallback). The first non-empty line that is not a `#` comment is the key, as issued; it is sent to the server on first use and re-read about every hour. Details — the C++ page, “License”. **Exceptions.** `dmr.DmrError` (a `RuntimeError`) — only a failure of the C ABI boundary itself. `ValueError` — invalid arguments (a value out of range, a closed scanner, `view()` of an image that did not load). `OSError` — the bundled library could not be loaded. Scan results and license refusals are values, not exceptions. **Message language.** Exception texts and the license `message` are in Russian or English: the `LIBSCANNER_LANG` environment variable (`ru` or `en`), otherwise the system language (English on any non-Russian system). Set the variable before importing the package. ### Other functions ```python def configure(server_url=None, app_version=None, timeout_ms=None, auto_check=True) -> None: ... def status() -> dict: ... def activate(key: str) -> dict: ... def refresh() -> dict: ... # check now instead of waiting a day def deactivate(reason: str | None = None) -> dict: ... # the release receipt goes to the server def deactivate_to_file(path, reason: str | None = None) -> dict: ... def write_activation_request(path) -> dict: ... def import_license_file(path) -> dict: ... # also works for renewals def fingerprint() -> str: ... # for support requests ``` To move to another machine, call `deactivate()` on the old one: the seat is released by a receipt from that machine. Every license server response is bound to its request: the package sends a random one-time number, the server returns it in a signed response, and a previously recorded response will not be accepted again. The receipt is also single-use — it contains the number from the last response received from the server, so the server will not accept a receipt from a restored copy of the machine's old state; the secret used to sign it is issued by the server only on the machine's first activation. For licenses with full offline autonomy (`full`), releasing a seat uses up the transfer limit; once it is exhausted, transfers are handled by support. A seat of a license can be released by yourself at most twice in 30 days in the limited mode and at most once in the 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](https://libscanner.com/en). ## Via subprocess When `dmr_c` cannot (or you would rather not) be built or installed, the `dmr` utility can be run as an external process — that route remains available and requires no build on the Python side: ```python import subprocess out = subprocess.run( ["dmr", "label.jpg"], capture_output=True, text=True, encoding="utf-8", ) print(out.returncode) # 0 — read, 1 — no code, 2 — not read, 3 — no license (table on the C++ page) print(out.stdout) # "startup: … ms\nprocessing: … ms\ncode: ……" — one "code:" line per code # (English on a non-Russian system; env={"LIBSCANNER_LANG": "ru"} for Russian) # a known set of files — one process, TAB-separated lines "path, code, ms, text" out = subprocess.run(["dmr", "--batch", "list.txt"], capture_output=True, text=True, encoding="utf-8") ``` For a frame stream from one conveyor, use `--serve` mode: one long-running process, images arrive one at a time, one line each on `stdin`, and the end of each response is marked by the line `#END#`. Below is a ready-made wrapper that puts it all in one place: ```python import subprocess import threading from dataclasses import dataclass @dataclass class DmrCode: rc: int elapsed_ms: float text: str class DmrServe: """Wrapper over "dmr --serve": one long-running process, one scanner. Not meant for calling scan() from several threads at once — the shell processes paths one at a time; without its own lock the responses would get mixed up between callers. For parallel PROCESSING of frames, use several DmrServe instances (several dmr processes). """ def __init__(self, dmr_path: str = "dmr", extra_args: list[str] | None = None): self._dmr_path = dmr_path self._extra_args = extra_args or [] self._lock = threading.Lock() self._proc: subprocess.Popen | None = None self._start() def _start(self) -> None: self._proc = subprocess.Popen( [self._dmr_path, "--serve", *self._extra_args], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8", bufsize=1, ) def scan(self, image_path: str) -> list[DmrCode]: with self._lock: if self._proc.poll() is not None: # The process has exited on its own — e.g. the license ran out # (Unlicensed stops --serve) or it was killed from outside. raise RuntimeError(f"dmr --serve is not running (code {self._proc.returncode})") self._proc.stdin.write(image_path + "\n") self._proc.stdin.flush() rows: list[DmrCode] = [] while True: line = self._proc.stdout.readline() if not line: raise RuntimeError("dmr --serve closed its output") line = line.rstrip("\n") if line == "#END#": break _path, rc, ms, text = line.split("\t") rows.append(DmrCode(rc=int(rc), elapsed_ms=float(ms), text=text)) return rows def close(self) -> None: with self._lock: if self._proc and self._proc.poll() is None: self._proc.stdin.close() self._proc.wait(timeout=5) def __enter__(self) -> "DmrServe": return self def __exit__(self, *exc) -> None: self.close() # usage: with DmrServe() as reader: for code in reader.scan("frame.jpg"): if code.rc == 0: print(code.text) ``` > **Non-ASCII paths on Windows.** The image path in the `--serve` line should be Latin or otherwise ASCII-safe: Cyrillic or other non-ASCII paths on Windows need to be re-encoded to the process's ANSI code page. The package itself (`dmr.Image`) does this re-encoding automatically. The license works the same way — the `--license ... --json` subcommands return the same JSON as `dmr.license`: ```python import json, subprocess out = subprocess.run( ["dmr", "--license", "status", "--json"], capture_output=True, text=True, encoding="utf-8", ) status = json.loads(out.stdout) if not status["can_scan"]: print(status["message"]) ``` --- # DataMatrix Generator 1.0 — C++ and C ABI > Generates DataMatrix ECC 200 symbols per ISO/IEC 16022 (GOST R ISO/IEC 16022) with GS1 and Chestny ZNAK profile validation — PNG and JPEG for marking print. Source page: https://libscanner.com/en/docs/datamatrix-generator/1.0 ## Overview The library builds **DataMatrix ECC 200** symbols according to ISO/IEC 16022 (GOST R ISO/IEC 16022) and returns them as a PNG or JPEG image — in memory or as a file. All 24 square sizes are supported, from 10×10 to 144×144, six encodation schemes (ASCII, C40, Text, X12, EDIFACT, Base256), and a GS1 option that validates the string against the GS1 General Specifications and the Chestny ZNAK profile (Chestny ZNAK is Russia's national product marking system). The library reads every symbol it builds back by itself (self-check): finder patterns, codewords, Reed–Solomon syndromes, scheme parsing. A symbol that fails to read back is never returned. The public interface is C++20, with headers in `include/dmgen/`. The public API does not throw exceptions: anything that can fail returns `Result`. For other languages there is a flat C ABI (`dmgen_c`) — suitable for P/Invoke, JNA, cgo and ctypes. > **Platforms.** Windows x86-64 and Linux x86-64, ARM64 and ARMv7 — the library is built and tested for single-board computers such as the Raspberry Pi as well. Third-party code is bundled in the distribution; there are no external dependencies. ## Linking The library ships prebuilt — only your program needs to be built. There are two ways to link it: **statically** — the C++ API from `libdmgen.a` becomes part of your program, or **dynamically** — through the `dmgen_c` library with a flat C interface (C ABI). Distribution contents: **Windows:** ``` include\dmgen\{Dmgen,Encoder,Gs1,Image,License,Options,Result,Symbol,Version}.h C++ API include\dmgen\dmgen_c.h C ABI lib\libdmgen.a static C++ library lib\pkgconfig\dmgen.pc bin\dmgen_c.dll C ABI library lib\dmgen_c.dll.a its import library (MinGW) bin\dmgen.exe command-line utility share\doc\dmgen\licenses\ third-party licenses ``` **Linux:** ``` include/dmgen/{Dmgen,Encoder,Gs1,Image,License,Options,Result,Symbol,Version}.h C++ API include/dmgen/dmgen_c.h C ABI lib/libdmgen.a static C++ library lib/pkgconfig/dmgen.pc lib/libdmgen_c.so -> libdmgen_c.so.1 -> libdmgen_c.so.1.0.3 C ABI library bin/dmgen command-line utility share/doc/dmgen/licenses/ third-party licenses ``` The `linux` (x86-64), `linux-arm64` and `linux-armhf` distributions have the same contents. | Your program | What to use | | --- | --- | | C++, built with MinGW-w64 GCC (Windows) or GCC (Linux) | Static linking — C++ API, `libdmgen.a` | | MSVC or another compiler; another language (C, C#, Java, Go…); updating the library without rebuilding the program | Dynamic linking — C ABI, `dmgen_c` | | Python | The wheel from Downloads — [Python documentation](https://libscanner.com/en/docs/datamatrix-generator/1.0/python.md) | ### Static linking — C++ API `libdmgen.a` becomes part of your program: nothing needs to be placed next to it except the key file `dmgen.key` (see "License"). You need a **C++20** compiler with the same C++ standard library as the distribution (the same or a newer version). **Windows:** `libdmgen.a` is built with MinGW-w64 GCC — MSVC cannot link it, you need the same toolchain: [MSYS2](https://www.msys2.org/), the **MINGW64** shell (not UCRT64), packages `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconf`. From MSVC, link dynamically, through dmgen_c. The link line comes from `dmgen.pc`; paths in it are relative to the file's own location, so the extraction directory can be anywhere: ```bash $env:PKG_CONFIG_PATH = "C:\extracted\to\lib\pkgconfig" pkg-config --static --cflags --libs dmgen ``` The system libraries `winhttp`, `crypt32`, `shell32` and `advapi32` (license check) are already listed in the package. **Linux:** The distribution is built with GCC 12 (Debian 12): you need GCC 12 or newer. The link line comes from `dmgen.pc`; the extraction directory can be anywhere: ```bash PKG_CONFIG_PATH=/extracted/to/lib/pkgconfig pkg-config --static --cflags --libs dmgen ``` `-pthread -ldl` are required — they are already in the package. In meson, use a dependency by package name: ```cpp dmgen_dep = dependency('dmgen', required: true, static: true) executable('myprogram', 'main.cpp', dependencies: [dmgen_dep], link_args: ['-static']) # Windows: MinGW runtime inside 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. ### Dynamic linking — C ABI `dmgen_c` is the same library behind a flat C interface (header `dmgen/dmgen_c.h`, details in C ABI). The compiler runtime is linked into it statically, so it depends neither on the caller's compiler nor on its C++ standard library. There is no separate `.pc` file for it — paths are specified directly. **Windows:** **MinGW-w64 GCC** — via the import library `lib\dmgen_c.dll.a`: ```bash gcc main.c -o myprogram.exe -I C:/extracted/to/include -L C:/extracted/to/lib -ldmgen_c ``` **MSVC** — there is no `.lib` file in the distribution; it is generated from the DLL itself. In an MSYS2 shell, create the export definition: ```bash gendef dmgen_c.dll # package mingw-w64-x86_64-tools; writes dmgen_c.def ``` then, in the Developer Command Prompt for VS (x64), build the import library and the program: ```bash lib /def:dmgen_c.def /machine:x64 /out:dmgen_c.lib cl /I C:\extracted\to\include main.c dmgen_c.lib ``` At run time, `dmgen_c.dll` must be next to your `.exe` (or in a directory on `PATH`), and the key file `dmgen.key` next to `dmgen_c.dll`. **Linux:** ```bash cc main.c -o myprogram -I/extracted/to/include -L/extracted/to/lib -ldmgen_c ``` At run time, the loader looks for `libdmgen_c.so.1`: either via `LD_LIBRARY_PATH`, or put it next to the program and set the search path at link time: ```bash LD_LIBRARY_PATH=/extracted/to/lib ./myprogram # or: the library next to the program cc main.c -o myprogram -I/extracted/to/include -L/extracted/to/lib -ldmgen_c -Wl,-rpath,'$ORIGIN' cp -a /extracted/to/lib/libdmgen_c.so.1* . ``` The key file `dmgen.key` goes next to `libdmgen_c.so`. The Python wheel is a wrapper over exactly `dmgen_c`, with the library inside the package: [Python documentation](https://libscanner.com/en/docs/datamatrix-generator/1.0/python.md). ## Quick start A Chestny ZNAK code → a PNG file. A single header is all you need: ```cpp #include #include int main() { dmgen::EncodeOptions o; o.gs1 = dmgen::Gs1Mode::ChestnyZnak; // validation and FNC1 as the first codeword auto sym = dmgen::encode("(01)04601234567893(21)5Abc12Xyz!-(93)dGVz", o); if (!sym) { std::cerr << sym.error().message << "\n"; // in the message language (“Errors”) return 1; } dmgen::RenderOptions r; // 8 px per module, quiet zone 2 auto saved = dmgen::save(dmgen::render(sym->modules, r), "code.png", dmgen::ImageFormat::Png); return saved ? 0 : 1; } ``` For an image in memory, use `dmgen::toPng(image)` or `dmgen::toJpeg(image)`; both return `Result>`. ## Encoding ```cpp Result encode(std::string_view data, const EncodeOptions& options = {}); ``` Bytes 128–255 are encoded via Upper Shift. Text is not re-encoded: a UTF-8 string is stored in the symbol as UTF-8. ### EncodeOptions | Field | Default | Meaning | | --- | --- | --- | | `side` | `0` | Symbol side: 0 — the smallest that fits, otherwise 10, 12, 14, 16, 18, 20, 22, 24, 26, 32, 36, 40, 44, 48, 52, 64, 72, 80, 88, 96, 104, 120, 132, 144. Not in the list — `InvalidArgument`, does not fit — `SizeTooSmall`. | | `encodation` | `Auto` | `Auto` — schemes are chosen by the look-ahead algorithm of Annex P of the standard; `Minimal` — the shortest stream (slower to compute); or a single scheme: `Ascii`, `C40`, `Text`, `X12`, `Edifact`, `Base256`. | | `gs1` | `Off` | `Off`, `Gs1`, `ChestnyZnak`. The string is validated before encoding; a violation gives `Gs1Invalid`. | | `separator` | `Gs29` | Group separator: `Gs29` — the GS byte, as in Chestny ZNAK; `Fnc1` — codeword 232 (only in `Gs1` mode). | | `gs1Input` | `Auto` | `Raw` — separator as byte 0x1D or written as ``, `{GS}`, `\x1D`; `Bracketed` — `(01)…(21)…`, separators are inserted automatically; `Auto` — decided by a leading "(". | | `verify` | `true` | Self-check: the built symbol is read back and compared with the input. | ### Symbol `info` — the chosen size (`SymbolInfo`), `modules` — the module matrix without the quiet zone (`BitMatrix`, 1 — dark), `codewords` — codewords in placement order, `segments` — which scheme covers which part, `text` — the string a reader will return. The size table is `dmgen::squareSizes()`. ## GS1 and Chestny ZNAK ```cpp auto rep = dmgen::gs1::validate("(01)04601234567894(21)ABC", dmgen::Gs1Mode::ChestnyZnak); for (const auto& i : rep.issues) std::cout << (i.warning ? "warning: " : "error: ") << i.message << "\n"; // error: AI (01) GTIN: check digit 4, must be 3 // error: Chestny ZNAK: no verification code; 93 or the pair 91 and 92 is required // (messages are in the message language — English on a non-Russian system, see “Errors”) ``` The checks cover known AIs, lengths and character sets, GTIN/SSCC/GLN check digits, dates, mandatory separators after variable-length AIs, and repeated AIs. The Chestny ZNAK profile: 01 first, 21 second, verification code 93 (4 characters) or 91 (4) + 92 (44 or 88). `Report::normalized` is the string exactly as a reader will return it (with the GS byte). GS1 validation does not require a license. ## Image | Function | What it does | | --- | --- | | `render(modules, RenderOptions)` | Gray8 raster: `modulePx` (8), `quietZone` (2; the standard requires at least 1), `invert`, `dark`/`light`, `dpi`. | | `RenderOptions::fromMm(mm, dpi)` | Module size in millimeters at the print resolution; the DPI is written to the file (pHYs, JFIF). | | `RenderOptions::jpegFriendly()` | Module size is a multiple of 8 px: JPEG 8×8 blocks are uniform, with no ringing at the edges. | | `toPng`, `toJpeg`, `save` | PNG (recommended) or JPEG, in memory or to a file; the path is a `std::filesystem::path`, Unicode works on Windows. JPEG quality below 90 is not recommended for barcodes. | ```cpp auto r = dmgen::RenderOptions::fromMm(0.5, 300); // 0.5 mm module at 300 dpi dmgen::save(dmgen::render(sym->modules, r), "label.png", dmgen::ImageFormat::Png); ``` ## Errors `Result` holds a value or an `Error` with the fields `code` and `message` (UTF-8, in the message language, see below). If you prefer exceptions, `value()` throws `std::runtime_error` with the error text. **Message language.** Messages are in Russian or English: the `LIBSCANNER_LANG` environment variable (`ru` or `en`), otherwise the system language — a Russian system gives Russian, any other (including the `C` locale in a container) gives English. From code — `dmgen::setMessageLanguage("en")` (`dmgen/Language.h`, for the whole process); `dmgen::messageLanguage()` returns the current one. The utility takes `--lang ru|en`. Error codes are not translated. | ErrorCode | Meaning | | --- | --- | | `InvalidArgument` | Invalid option: size not in the table, empty data… | | `DataTooLong` | The data does not fit even in 144×144 | | `SizeTooSmall` | The data does not fit in the explicitly specified size | | `UnencodableChar` | A character cannot be encoded with the chosen scheme | | `Gs1Invalid` | The string failed GS1 validation (details via `gs1::validate`) | | `IoError` | Failed to write the file or image | | `InternalVerifyFailed` | The self-check could not read the built symbol — a dmgen bug | | `Unlicensed` | The trial period has ended and there is no license | ## C ABI The library `dmgen_c.dll` / `libdmgen_c.so`, header `dmgen_c.h`; only `dmgen_*` symbols are exported. Fixed-size C types only, options carry a `struct_size`, memory is freed with `dmgen_free`; there is also a variant that writes into a caller-provided buffer — `dmgen_make_image_into`. ```cpp dmgen_options o; dmgen_error e; uint8_t* png; size_t len; dmgen_options_init(&o); o.gs1 = DMGEN_GS1_CHESTNY_ZNAK; if (dmgen_make_image((const uint8_t*)data, n, &o, DMGEN_FORMAT_PNG, &png, &len, &e) == DMGEN_OK) { /* … */ dmgen_free(png); } ``` `dmgen_options_init` fills the defaults and sets `struct_size` itself — call it first, then change fields. In a C string literal write the separator as its own literal — `"…LM" "\x1D" "93ABCD"`: in `"\x1D93"` the compiler takes all hex digits into one escape. On a failure `error` receives the code and a UTF-8 message in the message language (“Errors”). On Linux the library has the SONAME `libdmgen_c.so.1` (file `libdmgen_c.so.1.0.3`); there is no `.pc` file for `dmgen_c` — pass `-I…/include -L…/lib -ldmgen_c` yourself. The key file `dmgen.key` goes next to the library or next to the program (see “License”). ### Reference Every declaration of `dmgen_c.h` (ABI 1), with the header's own comments: ```cpp #define DMGEN_ABI_VERSION 1 /* Return codes: 0 is success, the rest match dmgen::ErrorCode. */ typedef int32_t dmgen_status; enum { DMGEN_OK = 0, DMGEN_E_INVALID_ARGUMENT = 1, DMGEN_E_DATA_TOO_LONG = 2, DMGEN_E_SIZE_TOO_SMALL = 3, DMGEN_E_UNENCODABLE_CHAR = 4, DMGEN_E_GS1_INVALID = 5, DMGEN_E_IO = 6, DMGEN_E_INTERNAL_VERIFY = 7, DMGEN_E_UNLICENSED = 8, /* trial period over and no license */ DMGEN_E_BUFFER_TOO_SMALL = 100 /* dmgen_make_image_into only */ }; enum { DMGEN_FORMAT_PNG = 0, DMGEN_FORMAT_JPEG = 1 }; enum { DMGEN_GS1_OFF = 0, DMGEN_GS1_GS1 = 1, DMGEN_GS1_CHESTNY_ZNAK = 2 }; enum { DMGEN_SEPARATOR_GS = 0, DMGEN_SEPARATOR_FNC1 = 1 }; enum { DMGEN_INPUT_AUTO = 0, DMGEN_INPUT_RAW = 1, DMGEN_INPUT_BRACKETED = 2 }; enum { DMGEN_SCHEME_AUTO = 0, DMGEN_SCHEME_ASCII, DMGEN_SCHEME_C40, DMGEN_SCHEME_TEXT, DMGEN_SCHEME_X12, DMGEN_SCHEME_EDIFACT, DMGEN_SCHEME_BASE256, DMGEN_SCHEME_MINIMAL }; /* Encoding and rendering options. Initialize with dmgen_options_init. */ typedef struct dmgen_options { uint32_t struct_size; /* sizeof(dmgen_options) as compiled by the caller */ int32_t gs1; /* DMGEN_GS1_* */ int32_t separator; /* DMGEN_SEPARATOR_* */ int32_t gs1_input; /* DMGEN_INPUT_* */ int32_t scheme; /* DMGEN_SCHEME_* */ int32_t side; /* 0 — smallest that fits, otherwise a standard side 10…144 */ int32_t verify; /* 1 — self-check (default) */ int32_t module_px; /* pixels per module, default 8 */ int32_t quiet_zone; /* modules, default 2 */ int32_t invert; /* 0/1 */ int32_t dark; /* 0…255, default 0 */ int32_t light; /* 0…255, default 255 */ int32_t jpeg_quality; /* 1…100, default 100 */ int32_t reserved; /* alignment, 0 */ double dpi; /* 0 — not written to the file */ } dmgen_options; typedef struct dmgen_error { int32_t code; /* dmgen_status */ char message[512]; /* UTF-8, null-terminated */ } dmgen_error; typedef struct dmgen_gs1_element { char ai[8]; /* null-terminated */ char* value; /* UTF-8, null-terminated */ size_t value_len; } dmgen_gs1_element; typedef struct dmgen_gs1_issue { int32_t code; /* dmgen::gs1::IssueCode */ int32_t warning; /* 1 — warning */ int32_t position; char ai[8]; char message[256]; } dmgen_gs1_issue; typedef struct dmgen_gs1_report { int32_t ok; int32_t element_count; dmgen_gs1_element* elements; int32_t issue_count; dmgen_gs1_issue* issues; char* normalized; /* with 0x1D bytes, as a reader returns it */ size_t normalized_len; } dmgen_gs1_report; uint32_t dmgen_abi_version(void); const char* dmgen_version_string(void); void dmgen_options_init(dmgen_options* options); /* Data → PNG or JPEG in memory. Free *out with dmgen_free. */ dmgen_status dmgen_make_image(const uint8_t* data, size_t len, const dmgen_options* options, int32_t format, uint8_t** out, size_t* out_len, dmgen_error* error); /* Same, into a caller-provided buffer. If cap is too small, returns * DMGEN_E_BUFFER_TOO_SMALL and sets *needed to the required size; buf may be * NULL when cap = 0. */ dmgen_status dmgen_make_image_into(const uint8_t* data, size_t len, const dmgen_options* options, int32_t format, uint8_t* buf, size_t cap, size_t* needed, dmgen_error* error); /* Data → file (UTF-8 path; Unicode works on Windows). */ dmgen_status dmgen_save_image(const uint8_t* data, size_t len, const dmgen_options* options, int32_t format, const char* path_utf8, dmgen_error* error); /* Data → side×side module matrix (1 is dark, rows top to bottom). * Free *modules with dmgen_free. */ dmgen_status dmgen_encode_matrix(const uint8_t* data, size_t len, const dmgen_options* options, uint8_t** modules, int32_t* side, dmgen_error* error); /* GS1 validation. Free *report with dmgen_gs1_report_free. The return code * tells whether the call itself succeeded; whether the string is valid is the * report's ok field. */ dmgen_status dmgen_gs1_validate(const uint8_t* data, size_t len, int32_t mode, int32_t input, dmgen_gs1_report** report, dmgen_error* error); void dmgen_gs1_report_free(dmgen_gs1_report* report); /* Sides of the 24 standard square sizes in increasing order; returns their * count (24) and writes at most cap values. */ int32_t dmgen_symbol_sizes(int32_t* sides, int32_t cap); void dmgen_free(void* p); /* ── License (dmgen/License.h) ────────────────────────────────────────────── * * Symbol-producing functions return DMGEN_E_UNLICENSED while there is neither * a trial period nor a license. The primary activation method is a dmgen.key * file containing the key, placed next to dmgen_c.dll / libdmgen_c.so: no * calls are needed. * * Responses are UTF-8 JSON, the same as "dmgen --license ... --json": * status {"state","can_encode","valid_until","refresh_after", * "offline_until","days_left","license_id","activation_id", * "offline_mode","clock_rollback","message"} * operation {"ok","error","message","status":{...status...}} * state is one of trial, active, refresh_due, trial_expired, expired, * network_required, wrong_machine, invalid. Free the string with dmgen_free; * NULL means an internal error or NULL in a required argument. Paths are * UTF-8. Network functions wait for the server at most timeout_ms. */ /* Call before the first network call and the first encoding; optional. * server_url, app_version — NULL or "" for the default; timeout_ms <= 0 — * 15000; auto_check — daily background check with the server (1 — on). */ void dmgen_license_configure(const char* server_url, const char* app_version, int32_t timeout_ms, int32_t auto_check); char* dmgen_license_status_json(void); char* dmgen_license_activate_json(const char* key); char* dmgen_license_refresh_json(void); char* dmgen_license_deactivate_json(const char* reason); /* reason may be NULL */ char* dmgen_license_deactivate_to_file_json(const char* path_utf8, const char* reason); char* dmgen_license_write_activation_request_json(const char* path_utf8); char* dmgen_license_import_license_file_json(const char* path_utf8); /* {"fingerprint":""} */ char* dmgen_license_fingerprint_json(void); ``` ## Library version `DMGEN_VERSION_STRING` is the header version, `dmgen::versionString()` is the version the library was built from. A mismatch shows that the headers and the `.a` come from different builds. ## License The library encodes while the trial period is running (one month from the first run on the machine, no network or registration needed) or a license is in effect. Otherwise `encode()` returns `ErrorCode::Unlicensed`. GS1 validation and rendering an existing matrix do not require a license. The license is bound to the hardware (motherboard, CPU, system disk), not to the OS installation; the trial period and the license are independent of the recognition library on the same machine. ```cpp #include dmgen::LicenseStatus s = dmgen::license::status(); if (!s.canEncode()) std::cerr << s.message; // "The trial period has ended. A license is required" dmgen::license::activate("XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"); // online dmgen::license::writeActivationRequest("request.json"); // offline: to your account … dmgen::license::importLicenseFile("license.lic"); // … and back dmgen::license::deactivate(); // moving to another machine ``` `deactivate()` releases the seat with a receipt from this machine. Every license server response is bound to its request: the library sends a random one-time number, the server returns it in a signed response, and a previously recorded response will not be accepted again. The receipt is also single-use — it contains the number from the last response received from the server, so the server will not accept a receipt from a restored copy of the machine's old state; the secret used to sign it is issued by the server only on the machine's first activation. For licenses with full offline autonomy (`full`), releasing a seat uses up the transfer limit; once it is exhausted, transfers are handled by support. A seat of a license can be released by yourself at most twice in 30 days in the limited mode and at most once in the extended mode; further transfers are handled by support. > **The main method is the dmgen.key file.** Put a text file `dmgen.key` containing the key next to `dmgen_c.dll` / `libdmgen_c.so` or next to the program. On the first call into the library the key is sent to the server — once; after that the license checks in with the server once a day on its own in a background thread, and encoding never waits for the network. ### States | LicenseState | Encodes | Meaning | | --- | --- | --- | | `Trial` | yes | The trial period is running | | `Active` | yes | The license is in effect | | `RefreshDue` | yes | No contact with the server for a while; the library retries hourly on its own | | `TrialExpired` | no | The trial period has ended — a license is required | | `Expired` | no | The paid term has ended; a renewal in your account is picked up automatically | | `NetworkRequired` | no | The offline period has run out — a connection to the server is required | | `WrongMachine` | no | The license was issued for another machine | | `Invalid` | no | The license record fails signature verification | The offline mode is set by the plan: `limited` — 7 days without a connection plus 7 more after a warning, `extended` — 30 + 90, `full` — never needs the network. ## Command line ```bash dmgen "0104601234567893215Abc12Xyz!-93dGVz" --gs1=cz -o code.png dmgen --check-gs1 --gs1=cz "(01)04601234567893(21)5Abc12Xyz!-(93)dGVz" dmgen --all-sizes "Hello" --out-dir sizes/ dmgen --batch codes.txt --out-dir out/ --gs1=cz --mm=0.5 --dpi=300 dmgen --license status # state, term, offline mode dmgen --license activate dmgen --license deactivate # moving to another machine ``` Options: `--size=`, `--scheme=`, `--separator=`, `--input=`, `--px=`, `--quiet=`, `--mm=` with `--dpi=`, `--invert`, `--jpeg-friendly`, `--quality=`. Without a license the utility exits with code 4. --- # DataMatrix Generator 1.0 — Python > Generates DataMatrix ECC 200 symbols per ISO/IEC 16022 (GOST R ISO/IEC 16022) with GS1 and Chestny ZNAK profile validation — PNG and JPEG for marking print. Source page: https://libscanner.com/en/docs/datamatrix-generator/1.0?lang=python ## Overview The `dmgen` package builds **DataMatrix ECC 200** symbols according to ISO/IEC 16022 (GOST R ISO/IEC 16022) and returns them as a PNG or JPEG image — as bytes or a file. All 24 square sizes, six encodation schemes, and string validation against GS1 and the Chestny ZNAK profile (Chestny ZNAK is Russia's national product marking system). The library reads every symbol back by itself before returning it. The wheel is platform-specific: the built library is inside the package. Wheels are available for Windows x86-64 and Linux x86-64, ARM64 and ARMv7 (including the Raspberry Pi). Python 3.8 or newer, no dependencies. ## Installation ```bash pip install libscanner-dmgen ``` The PyPI package is called `libscanner-dmgen` and is imported as `dmgen`. pip picks the wheel for your platform: Windows x86-64, Linux x86-64, ARM64 and ARMv7 (Raspberry Pi OS 64- and 32-bit). The Linux wheels need glibc 2.36 or newer (Debian 12, Ubuntu 23.04+, Raspberry Pi OS Bookworm). Python 3.8 or newer. The package also installs the `dmgen` command. Without access to PyPI, use the same wheel from the Downloads section of your account: **Windows:** ```bash pip install .\libscanner_dmgen-1.0.3-py3-none-win_amd64.whl ``` **Linux:** ```bash pip install ./libscanner_dmgen-1.0.3-py3-none-manylinux_2_36_x86_64.whl # x86-64 pip install ./libscanner_dmgen-1.0.3-py3-none-manylinux_2_36_aarch64.whl # ARM64 pip install ./libscanner_dmgen-1.0.3-py3-none-manylinux_2_36_armv7l.whl # ARMv7 ``` The library lives inside the package, in `dmgen/_lib/`, and is loaded only from there, by full path; there is no system search. The `DMGEN_LIB` variable (full path to the file) is only needed for a package built from source, where `_lib` is empty. ## Quick start ```python import dmgen # Chestny ZNAK code → file; the format follows the extension dmgen.save("(01)04601234567893(21)5Abc12Xyz!-(93)dGVz", "code.png", gs1="cz") # image in memory png = dmgen.make_image("Hello", module_px=8) jpg = dmgen.make_image("Hello", fmt="jpeg", quality=100) ``` ## Functions | Function | What it does | | --- | --- | | `make_image(data, *, fmt="png", **kw)` | Data → PNG or JPEG, `bytes` | | `save(data, path, *, fmt=None, **kw)` | Data → file; without `fmt` the format follows the extension (.jpg/.jpeg — JPEG, otherwise PNG) | | `encode_matrix(data, **kw)` | Module matrix: a list of rows from top to bottom, 1 — dark | | `validate_gs1(data, mode="gs1", gs1_input="auto")` | String validation, returns a `Report`. `mode`: `"gs1"` or `"cz"` (alias `"chestny_znak"`); `None`/`"off"` mean `"gs1"` here. `gs1_input`: `"auto"`, `"raw"` or `"bracketed"` | | `version()` | Version of the `dmgen_c` library | | `SIZES` | All 24 symbol sides, 10…144 | `data` is a `str` (encoded as UTF-8), `bytes`, `bytearray` or `memoryview`. `fmt` is `"png"` or `"jpeg"` (also `"jpg"`), case-insensitive. Option values are strings in lower or upper case; an unknown value raises `ValueError` before anything is encoded. ## Options | Keyword | Default | Meaning | | --- | --- | --- | | `gs1` | `None` | `None` (or `"off"`) — plain DataMatrix, the bytes are encoded as given; `"gs1"` — GS1 validation and FNC1 as the first codeword; `"cz"` (alias `"chestny_znak"`) — plus the Chestny ZNAK profile, separator only GS | | `separator` | `"gs"` | Group separator: the GS byte or `"fnc1"` (only with `gs1="gs1"`) | | `gs1_input` | `"auto"` | `"raw"` — separator is the byte `\x1d` or one of the text forms ``, `{GS}`, `\x1D`, `\u001D` (any case); a leading `]d2` or GS is dropped with a warning. `"bracketed"` — `(01)…(21)…`. `"auto"` — bracketed if the string starts with `(`, raw otherwise. Text forms of GS are recognised only with `gs1` on; without it `` is encoded literally | | `scheme` | `"auto"` | `"minimal"` — the shortest stream, or a single scheme: `ascii`, `c40`, `text`, `x12`, `edifact`, `base256` | | `side` | `0` | Symbol side from `SIZES`; 0 — the smallest that fits | | `verify` | `True` | Self-check by reading the symbol back | | `module_px` | `8` | Pixels per module, 1…1000 (less than 1 is treated as 1) | | `quiet_zone` | `2` | Quiet zone, in modules, 0…100 (negative is treated as 0); the standard requires at least 1. The whole image is at most 32768 px per side — larger raises `EncodeError` | | `invert`, `dark`, `light` | `False, 0, 255` | Inversion and module brightness | | `dpi` | `0` | Resolution in the file metadata; 0 — do not write. It does not change the pixel size of the image | | `quality` | `100` | JPEG quality; below 90 is not recommended for barcodes | **Size in millimetres.** The package has no millimetre option (C++ has `RenderOptions::fromMm`, the command line has `--mm`): compute the module size for the printer resolution yourself and write the same resolution into the file: ```python module_mm, dpi = 0.5, 300 dmgen.save(code, "code.png", gs1="cz", module_px=round(module_mm / 25.4 * dpi), dpi=dpi) # 6 px ``` For an image that is read from the file or the screen, the defaults (8 px per module, quiet zone 2 modules) are enough: the DataMatrix reader dmr reads codes from about 2 px per module. For JPEG keep the module a multiple of 8 px and the quality at 90 or more. ## GS1 and Chestny ZNAK ```python rep = dmgen.validate_gs1("(01)04601234567894(21)ABC", mode="cz") print(rep.ok) # False for i in rep.issues: print("warning:" if i.warning else "error:", i.message) # error: AI (01) GTIN: check digit 4, must be 3 # error: Chestny ZNAK: no verification code; 93 or the pair 91 and 92 is required # (English on a non-Russian system; LIBSCANNER_LANG=ru|en sets it, before the import) ``` `Report`: `ok`, `elements` (`ai`/`value` pairs), `issues`, `normalized` — the string with `\x1d`, as a reader will return it. Validation does not require a license. `ok` is `True` when there are no errors; warnings (`issue.warning`) do not prevent encoding — `save(..., gs1="cz")` writes the code and only reports them. GS1 rules (known AIs, lengths, character sets, GTIN check digit, duplicate AIs) apply in both modes; the Chestny ZNAK profile adds: - first element — AI 01 (GTIN, 14 digits with a valid check digit), second — AI 21 (serial number, 1–20 characters of GS1 set 82) — otherwise an error; - a serial number of unusual length is a *warning*, not an error: typical lengths are 6, 7, 8, 11, 12, 13 and 20; - a verification code is required after AI 21: AI 93 of exactly 4 characters, or the pair AI 91 (4 characters) and AI 92 (44 or 88 characters), 92 after 91; - AI 10, 11, 17, 240, 7003, 8005 and 310x are allowed without remarks; any other AI is a warning. ## Errors All exceptions derive from `dmgen.DmgenError` (a subclass of `RuntimeError`) with the fields `code` — an `int`, the error code of the C ABI (see the C++ page, “Errors”) — and `message` (in the message language: `LIBSCANNER_LANG` = `ru`/`en`, otherwise the system language). `Gs1Error` (code 5, the string failed GS1/Chestny ZNAK validation; adds `report`), `UnlicensedError` (code 8) and `EncodeError` (every other code) each derive directly from `DmgenError`. An unknown option value or a number out of range for a C `int` raises `ValueError` instead. ```python try: dmgen.save(code, "code.png", gs1="cz") except dmgen.Gs1Error as e: for i in e.report.issues: print(i.message) except dmgen.UnlicensedError as e: print(e.message) # the trial period has ended, no license except dmgen.EncodeError as e: print(e.message) # size, invalid character, file write… ``` ## License The package encodes while the trial period is running (one month from the first run on the machine, no network or registration needed) or a license is in effect; otherwise it raises `UnlicensedError`. The license is bound to the hardware, not to the OS installation. The main activation method is a text file `dmgen.key` containing the key in the `dmgen/_lib/` directory of the installed package: on first use the key is sent to the server, after which the license checks in with the server once a day on its own in the background. ```python import dmgen.license as lic s = lic.status() if not s["can_encode"]: print(s["message"]) lic.activate("XXXXX-XXXXX-XXXXX-XXXXX-XXXXX") # online lic.write_activation_request("request.json") # offline: to your account … lic.import_license_file("license.lic") # … and back lic.deactivate() # moving to another machine ``` `deactivate()` releases the seat with a receipt from this machine. Every license server response is bound to its request: the package sends a random one-time number, the server returns it in a signed response, and a previously recorded response will not be accepted again. The receipt is also single-use — it contains the number from the last response received from the server, so the server will not accept a receipt from a restored copy of the machine's old state; the secret used to sign it is issued by the server only on the machine's first activation. For licenses with full offline autonomy (`full`), releasing a seat uses up the transfer limit; once it is exhausted, transfers are handled by support. A seat of a license can be released by yourself at most twice in 30 days in the limited mode and at most once in the extended mode; further transfers are handled by support. The status is a dictionary: `state` (`trial`, `active`, `refresh_due`, `trial_expired`, `expired`, `network_required`, `wrong_machine`, `invalid`), `can_encode`, `valid_until`, `days_left`, `offline_mode`, `message` and others. Operations return `{"ok", "error", "message", "status"}` — a failure is not an exception but `ok: False` with an error code. --- # Machine-Readable Code Reader 1.0 — C++ and C ABI > 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. Source page: https://libscanner.com/en/docs/universal-reader/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: **Windows:** ``` 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 ``` **Linux:** ``` 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 lib/libunir_c.so C ABI library lib/pkgconfig/unir_c.pc bin/unir 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-.zip` (Windows) or `.tar.gz` (Linux) — the full package with libraries and headers, and `unir-1.0.1--cli` with the same extension — the `unir` utility only, for checks without programming. Platforms: `windows-x64`, `linux-x64`, `linux-arm64` and `linux-armhf` — the latter is ARMv7, a 32-bit OS (for example, a Raspberry Pi running a 32-bit system). An archive unpacks into a directory of the same name, for example `unir-1.0.1-linux-x64/`. | Your program | What to use | | --- | --- | | C++, built with MinGW-w64 GCC (Windows) or GCC (Linux) | Static linking — C++ API, `libunir.a` | | MSVC or another compiler; another language (C, C#, Delphi, Go…); updating the library without rebuilding the program | Dynamic linking — C ABI, `unir_c` | | Python | `pip install libscanner-unir` or the wheel from “Downloads” — [Python documentation](https://libscanner.com/en/docs/universal-reader/1.0/python.md) | ### 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). **Windows:** > **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](https://www.msys2.org/) — run the installer from the website, then in its **MINGW64** shell (the package is built in exactly this environment, not UCRT64): > > ```bash > 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: ```bash $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. **Linux:** The package is built with GCC 12 (Debian 12, glibc 2.36): GCC 12 or newer is required. Include paths and the link line come from `unir.pc`; point an environment variable at `lib/pkgconfig` of the unpacked package — the unpack directory can be anywhere: ```bash PKG_CONFIG_PATH=/path/to/unpacked/lib/pkgconfig pkg-config --static --cflags --libs unir ``` `-lpthread -ldl` are required — they are already in the package. The `.pc` does not add `-std=c++20`: pass it yourself (GCC 12 defaults to C++17). Without pkg-config the same line by hand: ```bash g++ -std=c++20 main.cpp -o myprogram -I/path/to/unpacked/include /path/to/unpacked/lib/libunir.a -pthread -ldl ``` OpenSSL is not a link dependency: for the license server's `https://` the library loads `libssl.so.3` (or `libssl.so.1.1`) at run time. > **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: ```cpp 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. **Windows:** **MinGW-w64 GCC** — via `unir_c.pc` and the import library `lib\unir_c.dll.a`, without `--static`: ```bash $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: ```bash 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: ```bash 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”). **Linux:** ```bash PKG_CONFIG_PATH=/path/to/unpacked/lib/pkgconfig pkg-config --cflags --libs unir_c ``` This yields `-I…/include -L…/lib -lunir_c`. At run time the loader must find `libunir_c.so`: either via `LD_LIBRARY_PATH`, or put it next to the program and set the search path at link time: ```bash LD_LIBRARY_PATH=/path/to/unpacked/lib ./myprogram # or: libunir_c.so next to the program cc main.c -o myprogram $(pkg-config --cflags --libs unir_c) -Wl,-rpath,'$ORIGIN' cp /path/to/unpacked/lib/libunir_c.so . ``` The key file `unir.key` goes next to `libunir_c.so` or next to the program (see “License”). In meson — likewise by package name, without `static`: ```cpp 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: ```cpp #include #include 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(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: **Windows:** ```cpp 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: ```bash 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: ```bash meson setup build --pkg-config-path="$PWD/unir-1.0.1-windows-x64/lib/pkgconfig" meson compile -C build ``` **Linux:** ```cpp project('test_unir', 'cpp', version : '0.1.0', default_options : ['cpp_std=c++20', 'warning_level=3', 'buildtype=release']) cxx = meson.get_compiler('cpp') unir_root = meson.current_source_dir() / 'unir-1.0.1-linux-x64' # or linux-arm64, linux-armhf unir_lib = cxx.find_library('unir', dirs : unir_root / 'lib', static : true) # Libs.private from unir.pc: -lpthread -ldl unir_dep = declare_dependency( include_directories : include_directories('unir-1.0.1-linux-x64/include'), dependencies : [unir_lib, dependency('threads'), cxx.find_library('dl')]) executable('test_unir', 'main.cpp', dependencies : unir_dep) ``` GCC 12 or newer and Meson are required; `-static` is not needed on Linux. ```bash meson setup build meson compile -C build ``` Or with pkg-config — `unir_dep = dependency('unir', static : true)` and: ```bash meson setup build --pkg-config-path="$PWD/unir-1.0.1-linux-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: ```cpp const unir::ImageView view{ m.data, m.cols, m.rows, (int)m.step, unir::PixelFormat::Bgr8 }; const unir::ScanResult r = scanner.scan(view); ``` ## Headers There are seven public C++ headers, all under `include/unir/`; the eighth, `unir/Unir.h`, includes them all at once. A ninth file, `unir/unir_c.h`, is a separate C interface (C ABI). The headers of the dmr and ssccr engines are not shipped: their types never appear in the interface. | Header | Contents | | --- | --- | | `unir/Unir.h` | Umbrella header — includes everything below | | `unir/Image.h` | `ImageView`, `Image`, `PixelFormat`, `ImageLimits`, `loadImage` | | `unir/Scanner.h` | `Scanner`, `ScanResult`, `Code`, `Status` | | `unir/Settings.h` | Scanner settings and presets | | `unir/Symbology.h` | `Symbology`, the `Symbologies` mask, names and AIM prefixes | | `unir/Geometry.h` | `Point`, `Box`, `Quad` — where the code is located in the frame | | `unir/License.h` | License check and activation, the `unir::license` namespace | | `unir/Version.h` | Version of the header (macros), of the built library and of the engines inside it | The headers pull in nothing extra: code that only builds a frame view needs just `Image.h`, and `Symbology.h` needs only `` and ``. ## 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. ```cpp 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 | `*load` | `status`, `timedOut` | C ABI / Python | | --- | --- | --- | --- | | Read | `Ok` | outcome of the scan | `UNIR_LOAD_OK` / `OK` | | No such file | `NotFound` | `InvalidImage`, `false` | `UNIR_LOAD_NOT_FOUND` / `NOT_FOUND` | | Not JPEG/PNG, or damaged | `Unreadable` | `InvalidImage`, `false` | `UNIR_LOAD_UNREADABLE` / `UNREADABLE` | | Decoding did not fit into the time limit | `Unreadable` | `InvalidImage`, `true` | `UNIR_LOAD_TIMED_OUT` / `TIMED_OUT` | | Exceeds the limits according to the header | `TooLarge` | `ImageTooLarge`, `false` | `UNIR_LOAD_TOO_LARGE` / `TOO_LARGE` | | No license — the file was not opened | `Ok` | `Unlicensed`, `false` | `UNIR_LOAD_OK` / `OK` | > **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 ```cpp // The numbers are the same as in dmr and ssccr. The exit codes of the unir utility are the // same numbers, except for file load failures (see "Command line"). enum class Status { Ok = 0, // at least one code read NotFound = 1, // no symbol found NotDecoded = 2, // symbol found but not read Unlicensed = 3, // no unir license, trial period over; the frame was not processed ImageTooLarge = 4, // frame larger than Settings::imageLimits, not processed InvalidImage = 5, // inconsistent frame description (row length, format) Failed = 6, // processing aborted by an internal error; the process lives on }; ``` `Ok` comes back if and only if `codes` holds at least one code — from any engine. If none of them read anything, the status is the weightiest of their answers: `Failed`, then `InvalidImage`, `ImageTooLarge`, `NotDecoded` and finally `NotFound`. This way a DataMatrix that was found but not read is not hidden behind a “no barcode” from the linear engine. With `Status::Unlicensed` the frame was not processed at all — the reason is given by `unir::license::status()` (see “License”). ### What NotDecoded really means `NotDecoded` does not mean “there definitely is a code”. It means that at least one engine in the mask found a *candidate* in the frame that looks like a symbol of its symbology, and could not read it. A candidate is not yet a symbol. The DataMatrix engine takes as a candidate anything that resembles a solid L finder pattern or a texture of modules — the bars of a linear code, printed text or a packaging pattern can all qualify. The linear engine flags a run of bars that partly matched the Code 128 or EAN/UPC pattern but failed acceptance. That is why status 2 is common on frames that contain no code of that symbology at all: a DataMatrix-only mask on a frame with a single GS1-128, an EAN/UPC-only mask on a frame with a single DataMatrix. The status is computed only from the engines in the mask: a code of a symbology outside the mask is not searched for, and its bars or modules can become a candidate for another engine — hence `NotDecoded` rather than `NotFound`. A linear code that the engine did read but the mask does not admit (an SSCC when the mask holds only `Code128`) is discarded and does not produce `Ok` either. Treat `NotDecoded` as “no code was read”: on a conveyor, take the next frame; for a one-off image, ask for a new shot or retry with a longer time limit. Do not use it as a sign that “the code is there but bad” (for rejecting products). If `timedOut` is set together with `NotFound` or `NotDecoded`, the answer is not final: the time limit cut the search short, and with more time the code might have been read. For your own program the statuses boil down to three outcomes: | Status | Outcome | What to do | | --- | --- | --- | | `Ok` | Code present | Use `codes`. With `timedOut`, some symbologies or codes may have gone unchecked | | `NotFound` | No code | Next frame. With `timedOut` — not final | | `NotDecoded` | No code (not read) | Next frame or a new shot; do not assume a code is present | | `Unlicensed` | Error | No license: show `license::status().message`; retrying the frame is pointless until activation | | `ImageTooLarge` | Input error | Downscale the frame or raise `imageLimits` | | `InvalidImage` | Input error | From `scan()` — a mistake in the frame description (your code); from `scanFile()` — the file was not read, the reason is in `*load` | | `Failed` | Error | Internal failure (for example, out of memory); the frame can be retried | ### ScanResult | Field | Type | Meaning | | --- | --- | --- | | `status` | `Status` | How processing of the frame ended | | `codes` | `std::vector` | The codes read, in reading order (see “Symbologies”). Empty if `status` is not `Ok`. No longer than `Settings::maxCodes` when that is greater than zero | | `timedOut` | `bool` | At least one engine was cut short by the frame time limit or never got to start. Success with the flag set is possible: a code was read, but some symbologies or hypotheses were not checked | | `elapsedMs` | `double` | How long the call took, ms. For `scanFile()` this includes loading the image | Methods: `bool ok() const` — shorthand for `status == Status::Ok`; `const std::string& text() const` — the text of the first code or an empty string. ### Code | Field | Type | Meaning | | --- | --- | --- | | `symbology` | `Symbology` | The code's symbology — exactly one bit of the mask (see “Symbologies”) | | `text` | `std::string` | The symbol data, bytes as they are. A leading FNC1 (the GS1 flag) is not passed on; an FNC1 inside is the GS separator, byte `0x1D`, not its printable form | | `box` | `Box` | Where the symbol was found in the original frame — a rectangle aligned with the frame axes | | `corners` | `Quad` | The symbol's four corners in the same frame, more precise than the box; the meaning of the indices depends on the symbology (see “Geometry”) | | `moduleCount` | `int` | DataMatrix: modules per symbol side; 0 — unknown or not a DataMatrix | | `modulePx` | `float` | Linear symbologies: module width in pixels; 0 — unknown or not a linear symbology | ### Text encoding `Code::text` holds the bytes the symbol carries; the library neither re-encodes them nor guesses their encoding. - **DataMatrix.** Bytes 0x00–0x7F are ASCII. Bytes 0x80–0xFF (Upper Shift, Base 256 mode) are returned as they are. By the DataMatrix standard such bytes default to ISO 8859-1 (Latin-1), but many generators put UTF-8 text into Base 256, so the decision is yours. ECI is not supported: an ECI switch is not interpreted, and the text of a symbol with ECI may contain service bytes; macros 05/06 are not expanded into the `[)>` header, and Structured Append is not assembled — each symbol is read on its own. Chestny ZNAK codes and GS1 DataMatrix use none of this: they contain only ASCII and 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 ```cpp 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. ```cpp 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 ``` ```cpp auto s = unir::Settings::stream(); s.symbologies = unir::Symbology::DataMatrix | unir::Symbology::Gs1_128; // marking codes and SSCC const unir::Scanner scanner(s); ``` | Symbology | What is read | | --- | --- | | `DataMatrix` | DataMatrix ECC200 of any structure — by the same engine and with the same answers as DataMatrix Reader | | `Gs1_128` | Code 128 with a leading FNC1. Without `Code128` in the mask only SSCC (AI 00) is read; together with `Code128`, other GS1-128 as well | | `Code128` | Code 128 without the GS1 flag. Acceptance is stricter than for SSCC: at least two scan lines must agree. Without `Gs1_128` in the mask, SSCC and other GS1-128 are discarded | | `Ean13`, `Ean8`, `UpcA`, `UpcE` | Acceptance requires two agreeing scan lines and quiet zones. UPC-A is 12 digits (an EAN-13 whose first digit is 0), UPC-E is 8 digits (number system, 6 digits, check digit), not expanded to UPC-A. The engine reads all four together, but only those in the mask are returned | > **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 ```cpp // 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 ```cpp 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. ```cpp 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: ```cpp 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 ```cpp 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. ```cpp 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 ```cpp 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. ```cpp 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. | Preset | Use case | What differs | | --- | --- | --- | | `Settings::stream()` | A frame stream from one camera — a conveyor. The default | `mode = Stream`: the DataMatrix engine accumulates a symbol size hint across frames; one code | | `Settings::single()` | One-off, unrelated images — a folder, manual checks | `mode = Single`: no hint; the DataMatrix engine tries every standard size if it could not measure the size; one code | | `Settings::multi(n)` | Several codes in one frame | `mode = Multi`, `maxCodes = n` (default 0 — as many as found) | ```cpp 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 ```cpp // unir/Settings.h struct Settings { static Settings stream(); // frame stream from one camera (the default) static Settings single(); // unrelated frames static Settings multi(int maxCodes = 0); // up to maxCodes codes per frame (0 — all) Symbologies symbologies = kAllSymbologies; // which symbologies to look for int maxCodes = 1; // 1 — up to the first code, N — up to N, 0 — all double budgetMs = 0.0; // frame time limit, ms, one for all symbologies; 0 — none double timeoutMs = 10000.0; // safety net, ms; 0 — the 60 s ceiling ImageLimits imageLimits{}; // 16384 per side, 100,000,000 pixels enum class Mode { Stream, Single, Multi }; Mode mode = Mode::Stream; // mode of the DataMatrix engine }; ``` The presets change only `mode`, and `multi()` also `maxCodes`; all other fields keep the defaults from the declaration. | Field | Default | Meaning | | --- | --- | --- | | `symbologies` | `kAllSymbologies` | Which symbologies to look for — a mask (see “Symbologies”). The default is every symbology of the build; future versions will have more | | `maxCodes` | 1 | How many codes to look for: 1 — up to the first code of any symbology, N — up to N, 0 — all | | `budgetMs` | 0.0 | Time limit per frame, ms — **one for all symbologies**: the engines share it in turn, and an engine with no time left does not start at all (`timedOut` is set). 0 — no limit, only `timeoutMs` applies. The limit is soft: frame time may exceed it by the duration of the stage in progress. The DataMatrix engine receives the budget as is, and, as in dmr itself, a budget changes how it searches, not only when it stops | | `timeoutMs` | 10000.0 | A safety net, ms: the same cut-off as `budgetMs` (the smaller of the two applies), but it does not change how the engines work. 0 and anything above 60000 mean the 60 s ceiling: a frame is never processed for longer. Protection against hostile frames, not a latency setting | | `imageLimits` | 16384 per side, 100,000,000 pixels | The largest frame the scanner agrees to process (`ImageLimits` from `unir/Image.h`; 0 — the hard ceiling of 65535 and 2^29). A larger frame immediately gets `Status::ImageTooLarge`. `scanFile()` checks the same limits against the file header before decoding. A safeguard, not a quality setting | | `mode` | `Mode::Stream` | Mode of the DataMatrix engine: `Stream` — size hint across frames, `Single` — unrelated frames with a size search, `Multi` — several codes. Usually set by the preset | unir does not expose the fine-grained dmr settings (rotation, GS1 check, candidate sources and the like): the DataMatrix engine runs with the dmr preset chosen by `mode`, plus your `maxCodes`, time limits and frame limits. ### Speed: mode, mask, time limit Frame time is determined by three settings, and all three are worth setting explicitly rather than left at their defaults. - **Mode.** With one code (`stream()`, `single()`) processing ends at the first code read, and the cheapest pass — SSCC and EAN/UPC — goes first: a frame with a single barcode is read quickly and DataMatrix is never reached. `multi(0)` cannot stop early: every engine in the mask goes through the whole frame, the linear one looks for every Code 128 and EAN/UPC, the DataMatrix engine tries all of its candidates. On a frame with one code this takes several times longer than one-code mode, and on a slow machine an order of magnitude longer. If you know how many codes to expect, 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 ```cpp #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. ```cpp #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. ```cpp #include 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 | 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 ```cpp // 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: ```cpp const char* stateName(unir::LicenseState s) { switch (s) { case unir::LicenseState::Trial: return "trial"; case unir::LicenseState::Active: return "active"; case unir::LicenseState::RefreshDue: return "refresh_due"; case unir::LicenseState::TrialExpired: return "trial_expired"; case unir::LicenseState::Expired: return "expired"; case unir::LicenseState::NetworkRequired: return "network_required"; case unir::LicenseState::WrongMachine: return "wrong_machine"; case unir::LicenseState::Invalid: return "invalid"; } return "invalid"; } ``` | Field | Meaning | | --- | --- | | `state` | `LicenseState` | | `canScan()` | Whether the library reads right now — true for `Trial`, `Active` or `RefreshDue` | | `validUntil` | End of the trial period or of the paid term, Unix seconds UTC | | `refreshAfter` | When the token is considered stale and the license moves to `RefreshDue`; 0 — never goes stale (trial period, `full` mode) | | `offlineUntil` | Beyond this time the license does not work without contacting the server; 0 — no limit (`full` mode) | | `daysLeft` | Whole days until the nearest of the limits above; 0 — limit passed | | `licenseId` / `activationId` | IDs of the license and of the specific activation | | `offlineMode` | `limited \| extended \| full`; empty without a license | | `clockRollback` | The system clock is behind a time the machine has already seen — rolling back the clock does not extend the term | | `message` | The state in words, for display to the user | Functions that act on the license return a `LicenseResult`: `ok`, the failure code `error` (the server's answer as is — `LICENSE_NOT_FOUND`, `ACTIVATION_LIMIT_REACHED`, `LICENSE_REVOKED` and others — or the library's own: `KEY_INVALID`, `NETWORK`, `WRONG_PRODUCT` and others), `message` in words and `status` — the state after the operation. ### Activation: online and offline Online — a key from your account; the machine contacts the license server itself: ```cpp 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`: ```cpp 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 ```cpp 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](https://libscanner.com/en). ## 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 ```cpp #include #include /* 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. ```cpp /* ── 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 [options] unir --batch [options] list of paths, one per line unir --folder [options] every image in the directory (not recursive) unir --serve [options] one path per line from stdin unir --license license unir --version versions of unir and its engines ``` | Option | Meaning | | --- | --- | | `--sym LIST` | Comma-separated symbologies, names as in `parseSymbologies`: `dm`, `128`, `gs1` (`sscc`), `ean` (`ean-13`, `ean-8`, `upc-a`, `upc-e`), `all` — the default | | `--codes N` | How many codes to look for: 1 — the default, 0 — all; any value other than 1 selects the `multi(N)` preset | | `--single` | Images are unrelated — the `single()` preset; the default is a stream, `stream()` | | `--budget-ms N` | Time limit per image, ms, one for all symbologies; 0 — no limit | | `--timeout-ms N` | Safety net, ms (10000); 0 — the 60 s ceiling | | `--max-side N` / `--max-pixels N` | Largest image side (16384) and area (100000000) | | `--color` | Load the image in colour; grayscale by default | | `--corners` | In batch modes — the symbol's corners as a sixth column | | `--root D` | In `--serve`: read only images inside directory D | ### Single image ``` $ unir label.jpg processing: 24.9 ms code: datamatrix 0104627191145677215n5Gr,pHTGIU&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: ` line per code, with the symbology as given by `nameOf()`. In the text GS is shown as ``, TAB as ``, line breaks as spaces. If the time limit cut the search short, a line `budget: exhausted, search cut off` follows. Errors go to stderr. The outcome is the exit code. The numbers are the same as in `unir::Status`, with one difference: an image that failed to load (no such file, not JPEG/PNG, decoding did not fit into the time limit) gives 1, not 5 (`InvalidImage`) as `scanFile()` would return; an oversized image gives 4, the same as `ImageTooLarge`. A time limit that cuts short the scan of an image already loaded does not change the code: it is 1 or 2 (or 0 if something was read in time) plus the budget line. Invalid command-line options also give 1. | Code | Meaning | | --- | --- | | 0 | At least one code read (also when the time limit cut the search short but codes were found) | | 1 | No code found; also no such file, not JPEG/PNG, decoding ran out of time, invalid options; in `--serve` — a path outside the `--root` directory | | 2 | A candidate was found but not read — this does not mean the image holds a code (see “What NotDecoded really means”) | | 3 | No trial period and no license | | 4 | Image exceeds the limit | | 5 | Inconsistent frame; the utility does not return it for images from disk — a file that failed to load gives 1 | | 6 | Internal error; in `--serve` also an invalid symbology list in the request | ### Batch mode and streaming `--batch` and `--folder` run one process for the whole set of images known in advance and print results line by line to stdout, one line per code: ``` pathexit codemstextsymbology[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 `pathsym=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&93CuXU datamatrix #END# ``` ### License from the command line ``` unir --license status [--json] unir --license activate [--server URL] [--json] unir --license refresh [--server URL] [--json] unir --license request unir --license import unir --license deactivate [] [--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. --- # Machine-Readable Code Reader 1.0 — Python > 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. Source page: https://libscanner.com/en/docs/universal-reader/1.0?lang=python ## Overview The `unir` package reads codes of different symbologies in a frame with a single call: **DataMatrix (ECC200)** — Chestny ZNAK marking (Russia's national product marking system), **GS1-128** — SSCC on logistics labels, **Code 128**, and retail barcodes **EAN-13, EAN-8, UPC-A and UPC-E**. It suits conveyor camera frames and photos of labels alike. It is a `ctypes` wrapper over the library's flat C ABI (`unir_c`) — a direct in-process call, with no external program launched for every frame. The package has no external dependencies, only the standard library. The library itself (`unir_c.dll` on Windows, `libunir_c.so` on Linux) and the `unir` command-line utility ship prebuilt inside the wheel — there is nothing to compile. ## Installation ```bash pip install libscanner-unir ``` pip picks the wheel for your platform; one wheel per platform works with any Python 3.9 or newer. Without access to PyPI, use the same wheel from the Downloads section of your account: **Windows:** ```bash pip install .\libscanner_unir-1.0.1-py3-none-win_amd64.whl ``` **Linux:** ```bash pip install ./libscanner_unir-1.0.1-py3-none-manylinux_2_36_x86_64.whl # x86-64 pip install ./libscanner_unir-1.0.1-py3-none-manylinux_2_36_aarch64.whl # ARM64 pip install ./libscanner_unir-1.0.1-py3-none-manylinux_2_36_armv7l.whl # ARMv7 ``` The package is called `libscanner-unir` and is imported as `unir`. Platforms: Windows x64, Linux x86-64, ARM64 and ARMv7 (Raspberry Pi OS 64- and 32-bit). The Linux wheels need glibc 2.36 or newer: Debian 12, Ubuntu 23.04+, Raspberry Pi OS Bookworm. The package also installs the `unir` command into the environment (and `python -m unir`) — see Command line. Next to the wheels, “Downloads” also has archives for each platform: `unir-1.0.1-.zip` (Windows) or `.tar.gz` (Linux) — the library for programs in other languages, and `unir-1.0.1--cli` — the `unir` utility only, for checks without Python. The wheels do not need them. ARMv7 (a 32-bit OS) is labelled with the `linux-armhf` platform in “Downloads”; its wheel carries the `manylinux_2_36_armv7l` tag. The library lives inside the package, in `unir/_lib/`, and is loaded only from there, by full path: there is deliberately no search through `PATH` or the current directory, so that a foreign `unir_c.dll` planted there cannot run with your program's privileges. The `UNIR_C_LIBRARY` environment variable (full path to the file; a relative one raises `OSError`) is only needed when the package has no `_lib` — for an installed wheel it has no effect. A library with a different major interface version is rejected on import, also with `OSError`: the package and the library must come from the same release. ## Quick start All codes in an image file: ```python import unir result = unir.scan_file("label.jpg", unir.Settings.multi(0)) # 0 — as many as there are for code in result.codes: print(code.symbology_name, code.text) # datamatrix b'0104601234500470215...\x1d91...\x1d92...' # gs1-128 b'00246009834320769650' ``` Only the symbologies you need, and a time limit per frame: ```python s = unir.Settings.single() s.symbologies = unir.Symbology.DATAMATRIX | unir.Symbology.GS1_128 s.budget_ms = 200 scanner = unir.Scanner(s) # created once, not per frame result = scanner.scan_file("frame.png") if result.ok: print(result.codes[0].symbology_name, result.text) elif result.status in (unir.Status.NOT_FOUND, unir.Status.NOT_DECODED): print("no code read") # NOT_DECODED also means "no code" else: print("not read:", result.status.name) ``` A frame from someone else's buffer — no copy, just a view of it (the buffer must stay alive for the duration of `scan()`): ```python view = unir.ImageView(buf, width, height, stride, unir.PixelFormat.GRAY8) result = scanner.scan(view) ``` > **Create the scanner once, not per frame.** In frame-stream mode (`Settings.stream()`) the DataMatrix reader remembers the symbol size from frame to frame, and this hint has to outlive a single frame — create the `Scanner` once per frame stream. The first license check also happens when the scanner is created. Calls to one scanner from different threads are serialized; for parallel work, use one scanner per thread. ## Modules The public names are all available directly from the `unir` package — there is no need to import from submodules, except `unir.license`, which has its own namespace: | Name | What it is | | --- | --- | | `unir.Scanner` / `unir.scan` / `unir.scan_file` | The scanner object and one-off functions without an object | | `unir.ScanResult` / `unir.Code` / `unir.Status` | Result of scanning a frame, one decoded code, frame outcome | | `unir.Box` / `unir.Point` | Geometry of a decoded code — fields of `Code` | | `unir.Symbology` / `unir.symbology_name` / `unir.parse_symbologies` | Symbologies: mask bits, short names, mask from a string | | `unir.Image` / `unir.ImageView` | A frame — owning (loaded from disk) and a view of someone else's buffer | | `unir.PixelFormat` / `unir.channels_of` / `unir.LoadAs` / `unir.LoadStatus` | Pixel format and its channel count, load mode and load outcome | | `unir.Settings` / `unir.Mode` | Scanner settings, presets and the DataMatrix decoding mode | | `unir.UnirError` | Exception: a failure of the library itself, not a read failure | | `unir.license` | License submodule — `import unir.license as license` | | `unir.version()` / `unir.engine_versions()` / `unir.abi_version()` | Library version, versions of the engines inside it, version of the library interface | ## Scanner `Scanner` holds a library resource — release it explicitly with `close()` or use it as a context manager (`with`); `__del__` is a safety net, but the timing of garbage collection is not guaranteed. ```python class Scanner: def __init__(self, settings: Settings | None = None): """None — Settings.stream(): a frame stream from one camera.""" def scan(self, frame: ImageView) -> ScanResult: """Read the codes in a frame.""" def scan_file(self, path) -> ScanResult: """An image file (JPEG, PNG) and the codes in it under ONE frame deadline (budget_ms, timeout_ms; loading counts toward it). The image is read as grayscale. If it fails to load — status IMAGE_TOO_LARGE or INVALID, details in load_status.""" def close(self) -> None: ... def __enter__(self) -> "Scanner": ... def __exit__(self, *exc) -> None: ... def scan(frame: ImageView, settings: Settings | None = None) -> ScanResult: """One-off scan of a frame without an object; None — Settings.single().""" def scan_file(path, settings: Settings | None = None) -> ScanResult: """One-off scan of an image file: loading and decoding under one deadline; None — Settings.single().""" ``` Note the defaults: `Scanner()` without settings is a frame stream (`Settings.stream()`), while the one-off `unir.scan()` and `unir.scan_file()` are for unrelated images (`Settings.single()`): they have nothing to accumulate a hint across frames in. The one-off `unir.scan_file()` creates a scanner, scans the image and closes the scanner itself, and the result is made of plain Python objects that hold no library resources: there is nothing to free afterwards. For many files in a row it is cheaper to create one scanner — the one-off function sets one up on every call. If the image fails to load, `scan_file()` does not raise: the reason is in `load_status`, and the result status follows this table: | What happened to the file | `load_status` | `status`, `timed_out` | | --- | --- | --- | | Read | `OK` | outcome of the scan | | No such file | `NOT_FOUND` | `INVALID`, `False` | | Not JPEG/PNG, or damaged | `UNREADABLE` | `INVALID`, `False` | | Decoding did not fit into the time limit | `TIMED_OUT` | `INVALID`, `True` | | Exceeds the limits according to the header | `TOO_LARGE` | `IMAGE_TOO_LARGE`, `False` | | No license — the file was not opened | `OK` | `UNLICENSED`, `False` | `INVALID_ARGUMENT` never comes from `scan_file()`: the settings are validated earlier, when the scanner is created. ```python with unir.Scanner(unir.Settings.single()) as scanner: for path in ["photo-1.jpg", "photo-2.png"]: result = scanner.scan_file(path) print(path, result.status.name, [c.text for c in result.codes]) ``` `scan()` and `scan_file()` on a closed scanner raise `ValueError`. Settings are validated when the scanner is created: a value out of range or an unknown symbology bit also raises `ValueError`. > **Threads.** Calls to one `Scanner` from different threads are queued by a lock in the package — safe, but not parallel. For frames to be decoded in parallel, create one scanner per thread: the object is cheap and can be created in advance. ## Scan result ### Status ```python class Status(enum.IntEnum): OK = 0 # at least one code read NOT_FOUND = 1 # no symbol found NOT_DECODED = 2 # a symbol was found but not decoded UNLICENSED = 3 # no license and the trial is over; the frame was not decoded IMAGE_TOO_LARGE = 4 # frame larger than Settings.max_image_side / max_image_pixels INVALID = 5 # inconsistent frame description; for scan_file — image not read FAILED = 6 # decoding aborted by an internal error ``` `OK` always comes with at least one code; with any other status `codes` is empty. With `UNLICENSED` the frame is not decoded at all, and `scan_file()` does not even open the file (`load_status` is `OK` in that case). The status numbers match the exit codes of the `unir` utility, except for an image that failed to load: `scan_file()` gives `INVALID` (5), while the utility answers 1 (see Command line). If no engine read anything, the status is the weightiest of their answers: `FAILED`, then `INVALID`, `IMAGE_TOO_LARGE`, `NOT_DECODED` and finally `NOT_FOUND`. ### What NOT_DECODED really means `NOT_DECODED` 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 `NOT_DECODED` rather than `NOT_FOUND`. 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 `NOT_DECODED` 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 `timed_out = True` comes together with `NOT_FOUND` or `NOT_DECODED`, the answer is not final: the time limit cut the search short, and with more time the code might have been read. For your own program the statuses boil down to three outcomes: | Status | Outcome | What to do | | --- | --- | --- | | `OK` | Code present | Use `codes`. With `timed_out`, some symbologies or codes may have gone unchecked | | `NOT_FOUND` | No code | Next frame. With `timed_out` — not final | | `NOT_DECODED` | No code (not read) | Next frame or a new shot; do not assume a code is present | | `UNLICENSED` | Error | No license: show `license.status()["message"]`; retrying the frame is pointless until activation | | `IMAGE_TOO_LARGE` | Input error | Downscale the frame or raise `max_image_side` / `max_image_pixels` | | `INVALID` | Input error | From `scan()` — a mistake in the frame description; from `scan_file()` — the file was not read, the reason is in `load_status` | | `FAILED` | Error | Internal failure (for example, out of memory); the frame can be retried | ```python def outcome(result: unir.ScanResult) -> str: """'code' — a code is present, 'none' — no code, 'error' — an error.""" if result.ok: return "code" if result.status in (unir.Status.NOT_FOUND, unir.Status.NOT_DECODED): return "none" return "error" ``` ### ScanResult ```python @dataclass(frozen=True) class ScanResult: status: Status codes: list[Code] # in reading order, see Symbologies timed_out: bool # at least one engine was cut off by the frame deadline or never started elapsed_ms: float load_status: LoadStatus | None = None # load outcome for scan_file(); None for scan() @property def ok(self) -> bool: ... # status == Status.OK @property def text(self) -> bytes: ... # text of the first code, or b"" ``` A frame cut off by the deadline (`Settings.budget_ms` or `timeout_ms`) returns whatever was read in time, with `timed_out = True`. ### Code ```python @dataclass(frozen=True) class Code: symbology: Symbology # exactly one bit text: bytes # NOT str — FNC1 inside the data stays the 0x1D byte box: Box # x, y, width, height — axis-aligned rectangle, frame pixels corners: tuple # four Points in order around the symbol module_count: int # DataMatrix: modules per side (0 — unknown or a linear code) module_px: float # linear codes: module width in pixels (0 — unknown or DataMatrix) @property def symbology_name(self) -> str: ... # "datamatrix", "gs1-128", "ean-13"… class Box(NamedTuple): x: int y: int width: int height: int class Point(NamedTuple): x: float y: float ``` > **Bytes, not a string.** `Code.text` is `bytes`. The leading FNC1 (the GS1 flag) is not included in the text, and FNC1 inside the data — the GS separator — stays the `0x1D` byte as is, not replaced with a printable form. The text may also contain a zero byte. Decode it yourself in the way that suits your format: > > ```python > code.text.decode("ascii") # EAN/UPC and SSCC — digits only > code.text.replace(b"\x1d", b"").decode("ascii") # a Chestny ZNAK code for display > ``` What the text contains: - **GS1** (DataMatrix with a leading FNC1, GS1-128) — without the leading FNC1, separators are the `0x1D` byte. A marking code looks like this: `0104601234500470215xX…\x1d91…\x1d92…`. - **SSCC** — 20 digits starting with "00": `00246009834320769650`. - **EAN-13** — 13 digits, **EAN-8** — 8. - **UPC-A** — 12 digits. An EAN-13 with a leading zero is a UPC-A, and such a code always comes as `upc-a`, in 12 digits. - **UPC-E** — 8 digits (number system, 6 digits, check digit), not expanded to UPC-A. - **DataMatrix without GS1** — the symbol's bytes as they are, not re-encoded. 0x00–0x7F are ASCII; bytes 0x80–0xFF (Upper Shift, Base 256 mode) are passed through: by the DataMatrix standard they are ISO 8859-1 (Latin-1), but many generators put UTF-8 text into Base 256, and the library does not guess the encoding. 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, and Structured Append is not assembled. - **Code 128 without GS1** — code sets A, B, C and SHIFT; the text is always 7-bit (0x00–0x7F; code set A also yields control characters). FNC2, FNC3 and FNC4 are not supported: a symbol containing them is not read, so extended Latin-1 characters never appear in the text. Turning the text into a string for display: ```python def text_of(code: unir.Code) -> str: if code.symbology != unir.Symbology.DATAMATRIX: return code.text.decode("ascii") # Code 128, GS1-128, EAN/UPC — always 7-bit try: return code.text.decode("utf-8") # the common case for Base 256 except UnicodeDecodeError: return code.text.decode("latin-1") # the DataMatrix default; never fails ``` After `decode()` the `0x1D` separator stays as the character `"\x1d"`; for display replace it with, for example, `""`. The `corners` are more precise than `box`: the symbol may be rotated in the frame. For DataMatrix, `corners[3]` is the corner the detector took for the corner of the solid L-shaped finder pattern; fine for drawing overlays, not for conclusions about how the content is rotated. For Code 128, GS1-128 and EAN/UPC, `corners[0]` is on the start side, `corners[1]` on the stop side along the same edge of the bars, `corners[2]` and `corners[3]` on the opposite edge. ## 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. ```python class Symbology(enum.IntFlag): NONE = 0x00 DATAMATRIX = 0x01 # ECC200, ISO/IEC 16022 CODE128 = 0x02 # Code 128 without the GS1 flag GS1_128 = 0x04 # GS1-128; without CODE128 in the mask only SSCC (AI 00) is read EAN13 = 0x08 EAN8 = 0x10 UPCA = 0x20 # 12 digits UPCE = 0x40 # 8 digits, not expanded to UPC-A EAN_UPC = EAN13 | EAN8 | UPCA | UPCE ALL = DATAMATRIX | CODE128 | GS1_128 | EAN_UPC ``` ```python s = unir.Settings.stream() s.symbologies = unir.Symbology.DATAMATRIX | unir.Symbology.GS1_128 # Chestny ZNAK and SSCC s.symbologies = unir.Symbology.EAN_UPC # retail barcodes only scanner = unir.Scanner(s) ``` | Symbology | What is read | | --- | --- | | `DATAMATRIX` | DataMatrix ECC200, square symbols from 10×10 to 144×144 — by the same engine as DataMatrix Reader. Rectangular DMRE symbols are not read: they are not part of the Russian standard GOST R ISO/IEC 16022 or of Chestny ZNAK marking codes | | `GS1_128` | Code 128 with a leading FNC1. If `CODE128` is not in the mask, only SSCC (AI 00) is read — this is the fast path; together with `CODE128`, other GS1-128 codes too | | `CODE128` | Code 128 without the GS1 flag. Acceptance is stricter than for SSCC: at least two scanlines must agree. On a frame without a barcode this is the most expensive search of all — do not enable it unless you need it | | `EAN13`, `EAN8`, `UPCA`, `UPCE` | Acceptance requires two agreeing scanlines and quiet zones. The engine reads all four together, but only those in the mask are returned. EAN 2- and 5-digit add-ons are not read | > **The default is every symbology of the build.** `Settings.symbologies` defaults to `Symbology.ALL`, i.e. everything this build can read. Future versions will add QR and other symbologies, and `ALL` will start looking for them too — and spending frame time on them. If you need a fixed set, set the mask explicitly. ### Names ```python unir.symbology_name(unir.Symbology.EAN13) # "ean-13" unir.symbology_name(unir.Symbology.EAN_UPC) # "unknown" — a mask, not a single bit code.symbology_name # the same for a decoded code unir.parse_symbologies("dm,gs1") # Symbology.DATAMATRIX | Symbology.GS1_128 unir.parse_symbologies("ean") # Symbology.EAN_UPC unir.parse_symbologies("qr") # ValueError: unknown symbology ``` Short names: `datamatrix`, `code128`, `gs1-128`, `ean-13`, `ean-8`, `upc-a`, `upc-e`; `0` gives `none`. `parse_symbologies` takes a comma-separated list — the same as the utility's `--sym` option: `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`. Handy when the set of symbologies comes from configuration. The GS1 flag (FNC1) of a DataMatrix is not reported in this version: tell a GS1-structured DataMatrix apart by the content of its text. ### Decoding order and code order - **One code** (`max_codes = 1`): first a fast pass over SSCC and EAN/UPC, then DataMatrix, then other Code 128. The first code read is the answer — the remaining engines are not called. - **Several codes** (`max_codes` 0 or greater than 1): all linear symbologies first, then DataMatrix. `codes` lists the linear codes first, then DataMatrix. Symbologies not in the mask do not take part in decoding and cost no time — narrowing the mask to what is really on your frames is the main way to save time. ## Frame ### PixelFormat ```python class PixelFormat(enum.IntEnum): GRAY8 = 0 # one channel: luminance; the cheapest for the scanner BGR8 = 1 # three channels, OpenCV order RGB8 = 2 # three channels BGRA8 = 3 # four channels, OpenCV and Windows order RGBA8 = 4 # four channels unir.channels_of(unir.PixelFormat.BGR8) # 3 ``` The scanner works with luminance and converts a color frame to it by itself. If your camera is monochrome, pass the frame as `GRAY8` rather than replicated into three channels: the answer is the same, and processing is cheaper. ### ImageView A view of someone else's buffer — the library does not copy it, free it or outlive it; the buffer must stay alive for the whole `scan()` call. The `ImageView` itself holds a reference to it. Any object supporting the buffer protocol will do: `bytearray`, a numpy array, `mmap`. ```python class ImageView: def __init__(self, buf, width: int, height: int, stride: int, format: PixelFormat): ... # stride — bytes per row; 0 — rows packed tightly (width * channels) width: int; height: int; stride: int; format: PixelFormat # read-only import numpy as np frame = np.zeros((480, 640), dtype=np.uint8) # a grayscale camera frame view = unir.ImageView(frame, 640, 480, frame.strides[0], unir.PixelFormat.GRAY8) result = scanner.scan(view) ``` Dimensions are checked before the library is called: a buffer shorter than a frame with the given `width`, `height`, `stride` and format needs, a `stride` smaller than `width` × channels, or a negative value raises `ValueError`. For non-writable buffers (`bytes`, a read-only memoryview) the package makes one explicit copy; writable ones are passed without copying. ### Image A frame owned by the library itself — loaded from disk. Formats: **JPEG and PNG**. Decode other formats yourself (for example with Pillow) and pass the pixels as `unir.ImageView`. ```python class LoadAs(enum.IntEnum): COLOR = 0 # PixelFormat.BGR8 GRAY = 1 # PixelFormat.GRAY8 — enough for the scanner class LoadStatus(enum.IntEnum): OK = 0 NOT_FOUND = 1 # no such file UNREADABLE = 2 # not an image, or corrupted TOO_LARGE = 3 # per its header, larger than Settings.max_image_side / max_image_pixels TIMED_OUT = 4 # decoding did not fit the deadline (budget_ms / timeout_ms) INVALID_ARGUMENT = 5 # settings rejected by the library class Image: def __init__(self, path, as_: LoadAs = LoadAs.GRAY, settings: Settings | None = None): ... status: LoadStatus def view(self) -> ImageView: ... def close(self) -> None: ... with unir.Image("frame.png", unir.LoadAs.GRAY) as img: if not img: print("failed to load:", img.status.name) else: view = img.view() result = scanner.scan(view) del view # the view keeps the frame: close() with a live view raises RuntimeError ``` A failed load does not raise: the object is falsy, `status` tells why, and `view()` on such an object raises `ValueError`. From `settings` the frame size limit and the decoding deadline are taken (the smaller of `budget_ms` and `timeout_ms`); `None` means the defaults: 16384 per side, 100 million pixels, 10 s. A frame above the limit is not decoded — the size is checked from the file header, before any pixels are read. `close()` while at least one view from `view()` is alive raises `RuntimeError`: the view points into the frame's memory. If you only need the result, `Scanner.scan_file()` is simpler: loading and decoding run under one frame deadline there, and the image is read straight into grayscale. > **Path encoding.** A path given as a Python string or `pathlib.Path` is passed to the library as UTF-8 — names with Cyrillic or any other characters open. Ready-made `bytes` go as is: on Windows that is UTF-8 or the system code page (`cp1251` on a Russian locale), on Linux the file system bytes of the name. ## Settings The presets are filled in by the library itself, not by numbers duplicated in Python: the defaults live in one place. Start from a preset and change fields as with any dataclass: ```python settings = unir.Settings.stream() # frame stream from one camera; the default of Scanner() settings = unir.Settings.single() # unrelated images: a folder, a file, a phone photo settings = unir.Settings.multi(0) # several codes per frame; 0 — as many as there are settings.budget_ms = 120.0 scanner = unir.Scanner(settings) ``` | Preset | When | What it does | | --- | --- | --- | | `stream()` | A frame stream from one camera: a conveyor, a handheld scanner | The DataMatrix reader remembers the symbol size between frames and does not re-try sizes; one code | | `single()` | Unrelated images | The DataMatrix reader tries the standard sizes if it could not measure the size; one code | | `multi(max_codes)` | Several codes per frame are needed | Up to `max_codes` codes, 0 — all | > **stream() is not for a folder of images.** For a set of unrelated images, `stream()` reads noticeably fewer DataMatrix codes: the size hint from the previous frame does not fit the next image and gets in the way. For files, use `single()` or `multi()`. ### Settings fields | Field | Default | Meaning | | --- | --- | --- | | `symbologies` | `Symbology.ALL` | Symbology mask, see Symbologies; an unknown bit raises `ValueError` | | `max_codes` | 1 | 1 — up to the first code of any symbology, N — up to N, 0 — all | | `mode` | `Mode.STREAM` | DataMatrix decoding mode: `STREAM`, `SINGLE`, `MULTI`; the presets set it themselves | | `budget_ms` | 0.0 | Time limit per frame, ms — one for all symbologies: the engines share it in turn, and loading the file in `scan_file()` counts toward it too. 0 — no limit (`timeout_ms` applies) | | `timeout_ms` | 10000.0 | Safety cutoff, ms: the same abort, but it does not change how the engines work; 0 — the 60 s cap | | `max_image_side` / `max_image_pixels` | 16384 / 100000000 | Largest frame to decode; larger — `Status.IMAGE_TOO_LARGE` without decoding. 0 — the library's hard cap: 65535 per side, 2²⁹ pixels | `budget_ms` and `timeout_ms` abort decoding the same way. The difference is that a set `budget_ms` also changes how the DataMatrix reader works (deadline-aware sampling), while `timeout_ms` is only a safeguard against a frame that takes too long. ### 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()`) decoding 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)`: decoding 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.** `budget_ms` 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 the result has `timed_out = True`. Codes read before the limit stay in `codes`. Without `budget_ms` a frame is bounded only by the `timeout_ms` 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 `scan_file()` loading the file counts against the same limit: ```python s = unir.Settings.multi(0) s.symbologies = unir.Symbology.DATAMATRIX | unir.Symbology.GS1_128 # only what you need s.budget_ms = 3000 # loading and decoding — no longer than ~3 s result = unir.scan_file("label.jpg", s) ``` ## Library version ```python print(unir.version()) # "1.0.1" — version of the built library print(unir.engine_versions()) # "dmr 1.0.12, ssccr 0.1.0" — the engines inside it print(unir.abi_version()) # "1.0" — version of the library interface, separate from the algorithm version ``` ## License The library reads codes while the trial period is running (30 days from the first run on the machine, no registration and no network needed) or a license is active. Otherwise `scan()` and `scan_file()` immediately return `Status.UNLICENSED`. The license is bound to the hardware (motherboard, CPU, system disk), not to the OS installation: reinstalling Windows or replacing the disk or CPU does not revoke it, replacing the motherboard does. The unir trial and license are its own, independent of other libscanner products on the same machine. ```python import unir.license as license s = license.status() if not s["can_scan"]: print(s["message"]) # the state in words, for showing to the user r = license.activate("UNIR-XXXXX-XXXXX-XXXXX-XXXXX") if not r["ok"]: print(r["error"], r["message"]) ``` A failed operation (wrong key, network unavailable) is not an exception but an ordinary `dict` with the reason. `unir.UnirError` is raised only when the library itself fails. ### The unir.key file The main way to activate is to write the license key (personal account, license page — “Show key”) into a text file named `unir.key` and put it in the `unir/_lib/` directory of the installed package, next to the library and the utility (they share the key). This prints the directory: ```bash python -c "import unir, os; print(os.path.join(os.path.dirname(unir.__file__), '_lib'))" ``` If the file is not there, the library also looks for `unir.key` next to the process executable; the current directory and environment variables are not consulted. The key is the first line of the file that is neither empty nor a `#` comment, exactly as issued; a file larger than 4 KB is not read. The name is its own, not `license.key` as with DataMatrix Reader: keys of different products cannot get mixed up. Nothing needs to be called: the key is activated over the network by itself on the first use of the library — creating the first `Scanner` or the first `unir.license` call; that call waits for the server's answer once. If it fails, the reason is in `status()["message"]`; without network the attempt is repeated hourly, after a server refusal — daily. A file placed while the program is running is picked up within an hour. ### Where the state is stored The trial and license state is kept in several copies; the earliest trial start wins: **Windows:** ```bash %ProgramData%\unir\license.dat %LOCALAPPDATA%\unir\license.dat HKCU\Software\unir, value state ``` **Linux:** ```bash /var/lib/unir/license.dat ~/.config/unir/license.dat ``` On Linux the home directory is taken from the system's user database, not from `$HOME`: overriding the variable does not give a new trial period. 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. 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. The trial period starts on the first call into the library on the machine — creating the first `Scanner` or the first `license.status()` — and is written to disk right away, not when the program exits. ### What the functions return `status()` returns a state dictionary; functions that do something with the license return an operation dictionary whose `status` is the state after the operation. It is the same JSON as `unir --license … --json` prints: ```python # license.status() {"state": "trial", "can_scan": True, "valid_until": 1795508197, "refresh_after": 0, "offline_until": 0, "days_left": 30, "license_id": "", "activation_id": "", "offline_mode": "", "clock_rollback": False, "message": "..."} # license.activate(...), refresh(), deactivate(), ... {"ok": False, "error": "KEY_INVALID", "message": "...", "status": {...}} ``` | Key | Meaning | | --- | --- | | `state` | `trial` — trial period; `active` — the license is active; `refresh_due` — active, but there has been no contact with the server for a long time; `trial_expired`; `expired` — the paid term is over; `network_required` — cannot continue without contacting the server; `wrong_machine` — a license of another machine; `invalid` — the record fails signature verification | | `can_scan` | Whether the library reads right now: `True` for `trial`, `active` and `refresh_due` | | `valid_until` | End of the trial or the paid term, Unix seconds UTC | | `refresh_after` / `offline_until` | From when a check with the server is due, and beyond when the license stops working without contact; 0 — no limit | | `days_left` | Whole days until the nearest of these limits; 0 — the limit has passed | | `license_id` / `activation_id` / `offline_mode` | License, activation and offline mode (`limited`, `extended`, `full`); empty without a license | | `clock_rollback` | The machine's clock is behind time already seen — rolling the clock back does not extend the term | | `message` | The state or the operation outcome in words, for showing to the user (in Russian) | | `ok` / `error` | Operation outcome and failure code: the server's answer as is (`LICENSE_NOT_FOUND`, `ACTIVATION_LIMIT_REACHED`, `LICENSE_REVOKED` and others) or a local one (`KEY_INVALID`, `NETWORK`, `NO_LICENSE`, `WRONG_PRODUCT` and others) | ### Functions ```python def configure(server_url=None, app_version=None, timeout_ms=None, auto_check=True) -> None: ... def status() -> dict: ... def activate(key: str) -> dict: ... # online, key UNIR-XXXXX-... def refresh() -> dict: ... # check now, without waiting a day def deactivate(reason: str | None = None) -> dict: ... # unbind over the network def deactivate_to_file(path, reason: str | None = None) -> dict: ... # unbind without network def write_activation_request(path) -> dict: ... # offline activation request def import_license_file(path) -> dict: ... # accept license.lic; also for renewal def fingerprint() -> str: ... # machine fingerprint, hex — for support ``` Call `configure()` before creating the first `Scanner`, if at all: activating the key from `unir.key` happens on the first use of the library and takes the server and `app_version` (your program's version, shown in the account next to the machine) from here. `timeout_ms` is the wait limit for one request to the server, 15 s by default. A license activated over the network checks in with the server by itself once a day in a background thread; reading a frame never waits for the network. Turn `auto_check` off only if your program must not go online on its own by its rules — then `limited` and `extended` licenses are renewed by the program's own `refresh()` call. Functions that use the network block the caller until the answer arrives or `timeout_ms` expires. ### Activation without internet For a machine without network: the request file is moved to a machine with internet access and uploaded in your account (Licenses → Offline activation), and you get `license.lic` back: ```python r = license.write_activation_request("request.json") # → to the account, get license.lic r = license.import_license_file("license.lic") # the same call renews the license ``` A license imported from a file has no key, so `refresh()` will not help it — it is renewed with a new `license.lic`. ### Moving to another machine `deactivate()` on the old machine frees the seat on the server; without connectivity the license stays in place. Without network — `deactivate_to_file("receipt.json")`: the license is removed from the machine immediately, and the receipt goes to a file that can be sent from any machine. ### Package exceptions `unir.UnirError` (a subclass of `RuntimeError`) — only a failure of the library itself. `ValueError` — invalid arguments: a value out of range, an unknown symbology, a short buffer, a closed scanner, `view()` on an image that failed to load. `RuntimeError` — `Image.close()` with a live view. `OSError` on `import unir` — the bundled library was not found or does not match. Scan results and license failures are values, not exceptions. > **Activation key.** The activation key is issued in your account after you purchase a plan in the [catalog](https://libscanner.com/en). ## Command line The wheel installs the `unir` command into the environment (the same as `python -m unir`): it is the prebuilt utility from `unir/_lib/`, and arguments and the exit code are passed through as is. Help — `unir --help`. From Python the utility is convenient to call via `subprocess` — for example, when you need a separate process or the ready-made batch mode. To avoid depending on `PATH`, run it with the current interpreter: ```python import subprocess, sys UNIR = [sys.executable, "-m", "unir"] out = subprocess.run([*UNIR, "--single", "label.jpg"], capture_output=True, text=True, encoding="utf-8") print(out.returncode) # Status: 0 read, 1 not found or file cannot be opened, 2 not decoded, 3 no license... print(out.stdout) # processing: … ms # code: gs1-128 00246009834320769650 ``` The utility's messages are in the system language (English on any non-Russian system; the `LIBSCANNER_LANG` variable or `--lang ru|en` sets it). In single-image mode stdout has the line `processing: … ms` and one line `code: ` per code; if the frame deadline ran out, also `budget: exhausted, search cut off`. The GS byte is printed as ``. The exit code is the `Status` number: 0 read, 1 not found or the file cannot be opened, 2 a candidate found but not decoded (see What NOT_DECODED really means), 3 no license, 4 image above the limit, 5 inconsistent frame, 6 internal error. One difference from `Status`: an image that failed to load (no such file, not JPEG/PNG, decoding did not fit into the time limit) gives 1, not 5 like `INVALID` from `scan_file()`; so the utility never returns 5 for images from disk. An oversized image gives 4, like `IMAGE_TOO_LARGE`. A time limit that cuts short the scan of an image already loaded does not change the code: 1 or 2 (or 0 if something was read in time). Invalid options also give 1; in `--serve` a path outside `--root` gives a line with code 1, and an invalid symbology list in the request — a line with code 6. ### Options | Option | What | | --- | --- | | `--batch LIST` | A file with a list of paths, one per line (UTF-8) | | `--folder DIR` | All JPEG and PNG images in a directory, not recursive, by name | | `--serve` | A path per line from stdin, until the input is closed | | `--sym LIST` | Comma-separated symbologies, as in `parse_symbologies`: `dm`, `128`, `gs1` (`sscc`), `ean`, `all` (default) | | `--codes N` | How many codes to look for: 1 (default), 0 — all | | `--single` | Frames are unrelated; the default is a frame stream. Use this option for a folder of photos | | `--budget-ms N` / `--timeout-ms N` | Like `Settings.budget_ms` and `timeout_ms` | | `--max-side N` / `--max-pixels N` | Image size limits | | `--color` | Read the image in color; by default — in grayscale | | `--corners` | In batch modes — symbol corners as a sixth column | | `--root DIR` | In `--serve` — only images inside this directory | | `--version` | Versions of unir and its engines | ### Batch output In `--batch`, `--folder` and `--serve` — one line per code, TAB-separated columns: ```bash path exit code ms text symbology [corners] ``` An image without codes gives one line of four columns with empty text and no symbology. The GS byte in the text is ``, a tab is ``. Corners (`--corners`) are four `x,y` pairs separated by spaces. The last stderr line is a summary, `[BATCH] images N, read M, …`. The exit code of batch mode is 0 if the batch was processed; without a license processing stops at the first image with code 3. ```python import subprocess, sys out = subprocess.run([sys.executable, "-m", "unir", "--batch", "list.txt", "--single"], capture_output=True, text=True, encoding="utf-8") for line in out.stdout.splitlines(): path, rc, ms, text, *rest = line.split("\t") symbology = rest[0] if rest else "" print(path, int(rc), float(ms), symbology, text) ``` ### Image stream: --serve One long-lived process; images arrive one at a time, a line each on stdin; after each image's lines comes a `#END#` line, and output is flushed immediately. A request line is a path or `pathsym=LIST`: symbologies for this image only (as in `--sym`; an invalid list gives a line with code 6). Paths are UTF-8. Without a license the process answers with a line with code 3 and `#END#`, and exits with code 3. ```python import subprocess, sys, threading class UnirServe: """One "unir --serve" process — one scanner. scan() calls from different threads are serialized; for parallel processing use several processes.""" def __init__(self, *args: str): self._lock = threading.Lock() self._proc = subprocess.Popen( [sys.executable, "-m", "unir", "--serve", *args], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, encoding="utf-8", bufsize=1, ) def scan(self, path: str, sym: str = "") -> list: """Answer rows: (exit code, symbology, text) per code.""" with self._lock: if self._proc.poll() is not None: raise RuntimeError(f"unir --serve has exited (code {self._proc.returncode})") self._proc.stdin.write(path + (f"\tsym={sym}" if sym else "") + "\n") self._proc.stdin.flush() rows = [] while True: line = self._proc.stdout.readline() if not line: raise RuntimeError("unir --serve closed its output") line = line.rstrip("\n") if line == "#END#": return rows _path, rc, _ms, text, *rest = line.split("\t") rows.append((int(rc), rest[0] if rest else "", text)) def close(self) -> None: with self._lock: if self._proc.poll() is None: self._proc.stdin.close() self._proc.wait(timeout=5) def __enter__(self) -> "UnirServe": return self def __exit__(self, *exc) -> None: self.close() with UnirServe("--single") as reader: for rc, symbology, text in reader.scan("frame.jpg"): if rc == 0: print(symbology, text) ``` ### License from the command line ```bash unir --license status [--json] unir --license activate [--json] unir --license refresh [--json] unir --license request unir --license import unir --license deactivate [] [--json] # with a file — no network, receipt to the file unir --license fingerprint [--json] ``` With `--json` the subcommands print the same JSON that `unir.license` returns. The exit code of `status` is 0 if the library reads and 3 if not; for operations — 0 done, 1 failed. The `--server URL` option overrides the license server for that call. ```python import json, subprocess, sys out = subprocess.run([sys.executable, "-m", "unir", "--license", "status", "--json"], capture_output=True, text=True, encoding="utf-8") status = json.loads(out.stdout) if not status["can_scan"]: print(status["message"]) ```