# Библиотека чтения машиночитаемых кодов 1.0 — C++ и C ABI

> Один вызов читает на кадре DataMatrix, GS1-128 (SSCC), Code 128, EAN-13, EAN-8, UPC-A и UPC-E — маркировку «Честного знака», логистические этикетки и товарные штрихкоды.

Страница на сайте: https://libscanner.com/docs/universal-reader/1.0

## Обзор

Библиотека одним вызовом читает на кадре коды разных символик: **DataMatrix (ECC200)** — маркировку «Честного знака», **GS1-128** — SSCC логистических этикеток, **Code 128** и товарные штрихкоды **EAN-13, EAN-8, UPC-A и UPC-E**. Точность и скорость чтения — вверху страницы; ниже — только устройство интерфейса.

Библиотека статическая, публичный интерфейс — на C++20, восемь заголовков в `include/unir/`, плюс плоский интерфейс на C для других языков (`unir_c.h`, раздел C ABI). Кадр передаётся видом на чужой буфер (`unir::ImageView`) — без копирования, из уже имеющегося буфера камеры или снимка.

## Подключение

Библиотека приходит уже собранной — собирать её не нужно, собирается только ваша программа. Подключить её можно двумя способами: **статически** — C++ API из `libunir.a` входит в вашу программу целиком, вместе с обоими движками, или **динамически** — через библиотеку `unir_c` с плоским интерфейсом на C (C ABI). Поставки есть для Windows x64 и Linux (x86-64, ARM64 и ARMv7), состав одинаковый:

**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++ (с движками dmr и ssccr)
lib\pkgconfig\unir.pc
bin\unir_c.dll               библиотека C ABI
lib\unir_c.dll.a             её библиотека импорта (MinGW)
lib\pkgconfig\unir_c.pc
bin\unir.exe                 утилита командной строки
share\doc\unir\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++ (с движками dmr и ssccr)
lib/pkgconfig/unir.pc
lib/libunir_c.so              библиотека C ABI
lib/pkgconfig/unir_c.pc
bin/unir                      утилита командной строки
share/doc/unir/licenses/      лицензии сторонних библиотек
```

В «Загрузках» личного кабинета на каждую платформу два архива: `unir-1.0.1-<платформа>.zip` (Windows) или `.tar.gz` (Linux) — полная поставка с библиотеками и заголовками, и `unir-1.0.1-<платформа>-cli` с тем же расширением — только утилита `unir`, для проверок без программирования. Платформы: `windows-x64`, `linux-x64`, `linux-arm64` и `linux-armhf` — это ARMv7, 32-битная ОС (например, Raspberry Pi с 32-битной системой). Архив распаковывается в каталог с тем же именем, например `unir-1.0.1-linux-x64/`.

| Ваша программа | Что брать |
| --- | --- |
| C++, собирается MinGW-w64 GCC (Windows) или GCC (Linux) | Статическая линковка — C++ API, `libunir.a` |
| MSVC или другой компилятор; другой язык (C, C#, Delphi, Go…); обновление библиотеки без пересборки программы | Динамическая линковка — C ABI, `unir_c` |
| Python | `pip install libscanner-unir` или колесо из «Загрузок» — [документация Python](https://libscanner.com/docs/universal-reader/1.0/python.md) |

### Статическая линковка — C++ API

`libunir.a` входит в вашу программу целиком: рядом с ней ничего класть не нужно, кроме файла ключа `unir.key` — он рядом с программой (раздел «Лицензия»). Нужен компилятор **C++20**. API на C++, в результате возвращаются `std::string` и `std::vector`, поэтому компилятор и его стандартная библиотека должны быть теми же, что и у поставки (той же или более новой версии).

**Windows:**

> **Нужен MinGW-w64 GCC, не MSVC.** Поставка для Windows собрана MinGW-w64 GCC — `libunir.a` в его формате и с его ABI. MSVC такой статический архив не слинкует; вашей программе нужен тот же тулчейн (например, MSYS2/MinGW или CLion с профилем MinGW). Из MSVC — динамически, через unir_c.
>
> Тулчейн бесплатный: [MSYS2](https://www.msys2.org/) — установщик с сайта, затем в его консоли **MINGW64** (поставка собрана именно в этом окружении, не в UCRT64):
>
> ```bash
> pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconf
> ```

Пути включения и строка линковки берутся из `unir.pc`. Путь к `lib\pkgconfig` распакованной поставки — через переменную окружения (PowerShell); пути в `.pc` считаются от места самого файла, так что каталог распаковки может быть любым:

```bash
$env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig"
pkg-config --static --cflags --libs unir
```

Системные `winhttp`, `crypt32`, `shell32` и `advapi32` (их использует проверка лицензии) уже прописаны в `Libs.private` пакета — отдельно подключать не нужно.

**Linux:**

Поставка собрана GCC 12 (Debian 12, glibc 2.36): нужен GCC 12 или новее. Пути включения и строка линковки — из `unir.pc`; путь к `lib/pkgconfig` распакованной поставки — через переменную окружения, каталог распаковки может быть любым:

```bash
PKG_CONFIG_PATH=/куда/распакован/lib/pkgconfig pkg-config --static --cflags --libs unir
```

Нужны `-lpthread -ldl` — они уже в пакете. `-std=c++20` файл `.pc` не добавляет — укажите сами (у GCC 12 по умолчанию C++17). Без pkg-config та же строка вручную:

```bash
g++ -std=c++20 main.cpp -o мояпрограмма -I/куда/распакован/include /куда/распакован/lib/libunir.a -pthread -ldl
```

OpenSSL в линковке не нужен: для `https://` сервера лицензий библиотека сама загружает `libssl.so.3` (или `libssl.so.1.1`) при работе.

> **Запрос к pkg-config должен быть статическим.** Поставка — одна статическая библиотека, а значит вызывающему нужны и те библиотеки, на которых стоит она сама. Без `--static` их в командной строке не окажется, и линковка упадёт на неразрешённых символах.

В meson — зависимость по имени пакета:

```cpp
unir_dep = dependency('unir', required: true, static: true)
executable('мояпрограмма', 'main.cpp', dependencies: [unir_dep],
  link_args: ['-static'])   # Windows: рантайм MinGW внутрь программы
```

Под Windows `-static` вкомпоновывает в программу и рантайм компилятора: без него она потребует рядом `libstdc++-6.dll`, `libgcc_s_seh-1.dll` и `libwinpthread-1.dll` из MSYS2. Под Linux он не нужен. Полный `meson.build` — в «Быстром старте».

### Динамическая линковка — C ABI

`unir_c` — та же библиотека за плоским интерфейсом на C (заголовок `unir/unir_c.h`): функции `unir_*` и непрозрачные указатели, без типов C++. Рантайм компилятора вкомпонован в неё статически, поэтому она не зависит ни от компилятора вызывающего, ни от его стандартной библиотеки C++ — годится для MSVC, Clang и любого языка с вызовом функций C. Всё, что библиотека отдаёт (результаты, кадры, строки), освобождается её же функциями: `unir_scan_result_free`, `unir_image_free`, `unir_free_string`.

Правила интерфейса и все функции — в разделе C ABI.

**Windows:**

**MinGW-w64 GCC** — через `unir_c.pc` и библиотеку импорта `lib\unir_c.dll.a`, без `--static`:

```bash
$env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig"
pkg-config --cflags --libs unir_c
```

Это даёт `-I…\include -L…\lib -lunir_c`.

**MSVC** — файла `.lib` в поставке нет, его делают из самой DLL. В консоли MSYS2 — описание экспорта:

```bash
gendef unir_c.dll         # пакет mingw-w64-x86_64-tools; пишет unir_c.def
```

затем в Developer Command Prompt for VS (x64) — библиотека импорта и сборка:

```bash
lib /def:unir_c.def /machine:x64 /out:unir_c.lib
cl /I C:\куда\распакован\include main.c unir_c.lib
```

При запуске `unir_c.dll` должна лежать рядом с вашим `.exe` (или в каталоге из `PATH`), а файл ключа `unir.key` — рядом с `unir_c.dll` или рядом с `.exe` (раздел «Лицензия»).

**Linux:**

```bash
PKG_CONFIG_PATH=/куда/распакован/lib/pkgconfig pkg-config --cflags --libs unir_c
```

Это даёт `-I…/include -L…/lib -lunir_c`. При запуске загрузчик должен найти `libunir_c.so`: либо через `LD_LIBRARY_PATH`, либо положите её рядом с программой и пропишите путь поиска при линковке:

```bash
LD_LIBRARY_PATH=/куда/распакован/lib ./мояпрограмма

# или: libunir_c.so рядом с программой
cc main.c -o мояпрограмма $(pkg-config --cflags --libs unir_c) -Wl,-rpath,'$ORIGIN'
cp /куда/распакован/lib/libunir_c.so .
```

Файл ключа `unir.key` — рядом с `libunir_c.so` или рядом с программой (раздел «Лицензия»).

В meson — так же по имени пакета, без `static`:

```cpp
unir_c_dep = dependency('unir_c', required: true)
executable('мояпрограмма', 'main.c', dependencies: [unir_c_dep])
```

## Быстрый старт

Минимальная программа: снимок с диска, чтение, вывод символики и текста. Всё, что нужно, — один заголовок:

```cpp
#include <unir/Unir.h>
#include <iostream>

int main() {
    unir::Scanner scanner;                 // поток кадров, все символики сборки, один код

    unir::LoadStatus load = unir::LoadStatus::Ok;
    const unir::ScanResult r = scanner.scanFile("кадр.jpg", &load);   // загрузка и разбор под одним сроком
    if (load != unir::LoadStatus::Ok) {
        std::cerr << "не удалось прочитать файл\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 << "код не прочитан\n";   // NotDecoded — тоже «нет кода»
    } else {
        std::cout << "кадр не прочитан, статус " << static_cast<int>(r.status) << "\n";
    }
}
```

Собрать пример можно Meson. Рядом с `main.cpp` — распакованная поставка (`unir-1.0.1-windows-x64` или `unir-1.0.1-linux-x64`) и файл `meson.build`:

**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')

# Готовая сборка unir лежит рядом с проектом; зависимость описана вручную —
# так pkg-config не нужен вовсе.
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 из unir.pc — нужны при статической компоновке
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'])
```

Сборка — в консоли MSYS2 MINGW64:

```bash
meson setup build
meson compile -C build
```

То же через pkg-config: `.pc`-файлы поставки перемещаемые, поэтому вместо ручного описания хватит одной строки в `meson.build` — `unir_dep = dependency('unir', static : true)` — и пути к распакованному `lib/pkgconfig` при настройке:

```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'   # или linux-arm64, linux-armhf

unir_lib = cxx.find_library('unir', dirs : unir_root / 'lib', static : true)

# Libs.private из 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 или новее и Meson; `-static` под Linux не нужен.

```bash
meson setup build
meson compile -C build
```

Или через pkg-config — `unir_dep = dependency('unir', static : true)` и:

```bash
meson setup build --pkg-config-path="$PWD/unir-1.0.1-linux-x64/lib/pkgconfig"
meson compile -C build
```

Для кадра из своего конвейера захвата (SDK камеры, буфер OpenCV, Windows-битмап) диск не участвует — собирается вид на уже имеющийся буфер, без копирования:

```cpp
const unir::ImageView view{ m.data, m.cols, m.rows, (int)m.step, unir::PixelFormat::Bgr8 };
const unir::ScanResult r = scanner.scan(view);
```

## Заголовки

Публичных заголовков C++ семь, все под `include/unir/`; восьмой, `unir/Unir.h`, включает их все разом. Девятый файл, `unir/unir_c.h`, — отдельный интерфейс на C (C ABI). Заголовков движков dmr и ssccr в поставке нет: их типы наружу не выходят.

| Заголовок | Что в нём |
| --- | --- |
| `unir/Unir.h` | Агрегат — включает всё нижеперечисленное |
| `unir/Image.h` | `ImageView`, `Image`, `PixelFormat`, `ImageLimits`, `loadImage` |
| `unir/Scanner.h` | `Scanner`, `ScanResult`, `Code`, `Status` |
| `unir/Settings.h` | Настройки и пресеты сканера |
| `unir/Symbology.h` | `Symbology`, маска `Symbologies`, имена и префиксы AIM |
| `unir/Geometry.h` | `Point`, `Box`, `Quad` — где на кадре расположен код |
| `unir/License.h` | Проверка и активация лицензии, пространство `unir::license` |
| `unir/Version.h` | Версия заголовка (макросы), собранной библиотеки и движков внутри неё |

Заголовки не тянут лишнего: тому, кто только собирает вид на кадр, достаточно одного `Image.h`, а `Symbology.h` нужен только `<cstdint>` и `<string>`.

## Scanner

`Scanner` — объект, а не функция, и создавать его на каждый кадр не нужно. В конструкторе собирается цепочка движков под заданные символики, и там же проходит первая проверка лицензии — если рядом лежит ещё не активированный `unir.key`, конструктор один раз ждёт ответа сервера, а первый кадр этой задержки уже не платит. В режиме потока (`Settings::stream()`) DataMatrix-движок копит между кадрами подсказку размера символа, и ей нужно пережить кадр.

```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;

    // Разбор кадра. Не потокобезопасен на одном Scanner в режиме Stream
    // (подсказка dmr меняется от кадра к кадру); для потоков — по Scanner
    // на поток.
    ScanResult scan(ImageView frame) const;

    // Снимок с диска и его разбор под ОДНИМ сроком кадра (загрузка в него
    // входит). load — что вышло с чтением файла (может быть nullptr).
    ScanResult scanFile(const std::string& path, LoadStatus* load = nullptr) const;

    const Settings& settings() const;
};

// Разовый разбор кадра без объекта: проба, скрипт, один файл.
ScanResult scan(ImageView frame, const Settings& s = Settings::single());
```

> **Один Scanner — один поток.** `scan()` объявлен `const`, но в режиме `Stream` он меняет подсказку размера DataMatrix от кадра к кадру. Если кадры приходят из нескольких потоков (несколько камер, пул обработчиков), заводите по своему `Scanner` на каждый поток, а не один общий.

`scanFile()` — снимок с диска и коды на нём под общим сроком: загрузка входит в тот же `budgetMs` / `timeoutMs`, что и разбор, и тяжёлый файл не получает второго бюджета. Снимок читается в полутон (`LoadAs::Gray`) с пределами из `Settings::imageLimits`. Если файл не загрузился, в `*load` приходит причина (раздел «Кадр»), а статус результата — `ImageTooLarge` для слишком большого снимка и `InvalidImage` для всех остальных отказов; если декодирование оборвал срок, взводится и `timedOut`. Без лицензии файл даже не открывается: `*load` остаётся `Ok`, статус — `Unlicensed`.

У `LoadStatus` в C++ четыре значения, и отдельного «не успел за срок» среди них нет: декодирование, оборванное сроком, — это `Unreadable` вместе с `timedOut = true`. В C ABI и Python тот же случай приходит отдельным значением `TIMED_OUT`; ещё одно значение, `INVALID_ARGUMENT` (настройки отвергнуты границей ABI), в C++ не нужно — тип `Settings` здесь ошибиться не даёт.

| Что с файлом | `*load` | `status`, `timedOut` | C ABI / Python |
| --- | --- | --- | --- |
| Прочитан | `Ok` | итог разбора | `UNIR_LOAD_OK` / `OK` |
| Файла нет | `NotFound` | `InvalidImage`, `false` | `UNIR_LOAD_NOT_FOUND` / `NOT_FOUND` |
| Не JPEG/PNG или повреждён | `Unreadable` | `InvalidImage`, `false` | `UNIR_LOAD_UNREADABLE` / `UNREADABLE` |
| Декодирование не уложилось в срок | `Unreadable` | `InvalidImage`, `true` | `UNIR_LOAD_TIMED_OUT` / `TIMED_OUT` |
| Больше пределов по заголовку | `TooLarge` | `ImageTooLarge`, `false` | `UNIR_LOAD_TOO_LARGE` / `TOO_LARGE` |
| Нет лицензии — файл не открывался | `Ok` | `Unlicensed`, `false` | `UNIR_LOAD_OK` / `OK` |

> **Ошибки возвращаются, а не бросаются.** `scan()` и `scanFile()` ловят исключения внутри: внутренний сбой (например, нехватка памяти) — это `Status::Failed`, если ни одного кода к этому моменту не прочитано, и процесс живёт дальше. `loadImage()` сообщает о проблемах через `LoadStatus`.

> **Move-only.** `Scanner` можно перемещать, но не копировать — конструктор и оператор копирования удалены явно.

## Результат чтения

### Status

```cpp
// Номера те же, что у dmr и ssccr. Коды выхода утилиты unir — те же числа, кроме
// отказов загрузки файла (раздел «Командная строка»).
enum class Status {
    Ok            = 0,  // прочитан хотя бы один код
    NotFound      = 1,  // ни одного символа не найдено
    NotDecoded    = 2,  // символ найден, но не прочитан
    Unlicensed    = 3,  // нет лицензии unir, пробный период кончился; кадр не разбирался
    ImageTooLarge = 4,  // кадр больше Settings::imageLimits, не разбирался
    InvalidImage  = 5,  // описание кадра противоречиво (длина строки, формат)
    Failed        = 6,  // разбор оборвался внутренней ошибкой; процесс жив
};
```

`Ok` приходит тогда и только тогда, когда в `codes` есть хотя бы один код — от любого движка. Если не прочитал ни один, статус — самый весомый из их ответов: `Failed`, затем `InvalidImage`, `ImageTooLarge`, `NotDecoded` и, наконец, `NotFound`. Так DataMatrix, найденный, но не прочитанный, не теряется за «штрихкода нет» от линейного движка. При `Status::Unlicensed` кадр не разбирался вовсе — причину даёт `unir::license::status()` (раздел «Лицензия»).

### Что на самом деле значит NotDecoded

`NotDecoded` — не «код точно есть». Это значит: хотя бы один движок маски нашёл на кадре *кандидата*, похожего на символ своей символики, и не смог его прочитать. Кандидат — ещё не символ. Движок DataMatrix берёт в кандидаты всё, что похоже на сплошную L-рамку или на текстуру модулей, — ими бывают и штрихи линейного кода, и текст, и узор упаковки. Линейный движок отмечает участок штрихов, который частично подошёл под шаблон Code 128 или EAN/UPC, но не прошёл приёмку. Поэтому ответ 2 обычен и на кадрах, где кода этой символики нет вовсе: маска только DataMatrix на кадре с одним GS1-128, маска только EAN/UPC на кадре с одним DataMatrix.

Статус считается только по движкам, которые есть в маске: код символики вне маски не ищется, и его штрихи или модули могут дать кандидата чужому движку — отсюда `NotDecoded`, а не `NotFound`. Линейный код, который движок прочитал, но маска не пропускает (SSCC при маске только `Code128`), отбрасывается и тоже не даёт `Ok`.

Относитесь к `NotDecoded` как к «кода не прочитано»: на конвейере — берите следующий кадр, на разовом снимке — просите переснять или повторите разбор с бо́льшим сроком. Как признак «код есть, но плохой» (для отбраковки) его не используйте. Если вместе с `NotFound` или `NotDecoded` взведён `timedOut`, ответ не окончательный: разбор оборвал срок, и с бо́льшим сроком код мог бы прочитаться.

Для своей программы статусы удобно свести к трём исходам:

| Status | Исход | Что делать |
| --- | --- | --- |
| `Ok` | Есть код | Брать `codes`. При `timedOut` часть символик или кодов могла остаться непроверенной |
| `NotFound` | Нет кода | Следующий кадр. При `timedOut` — не окончательно |
| `NotDecoded` | Нет кода (не прочитан) | Следующий кадр или пересъёмка; не считать, что код на кадре есть |
| `Unlicensed` | Ошибка | Нет лицензии: показать `license::status().message`; повторять кадр бесполезно до активации |
| `ImageTooLarge` | Ошибка входа | Уменьшить кадр или поднять `imageLimits` |
| `InvalidImage` | Ошибка входа | У `scan()` — ошибка в описании кадра (ваш код); у `scanFile()` — файл не прочитан, причина в `*load` |
| `Failed` | Ошибка | Внутренний сбой (например, нехватка памяти); кадр можно повторить |

### ScanResult

| Поле | Тип | Смысл |
| --- | --- | --- |
| `status` | `Status` | Чем кончился разбор кадра |
| `codes` | `std::vector<Code>` | Прочитанные коды в порядке чтения (раздел «Символики»). Пуст, если `status` не `Ok`. Не длиннее `Settings::maxCodes`, если тот больше нуля |
| `timedOut` | `bool` | Хоть один движок оборван по сроку кадра или не успел начать. Успех при взведённом флаге возможен: код прочитан, но часть символик или гипотез не проверена |
| `elapsedMs` | `double` | Сколько занял вызов, мс. У `scanFile()` сюда входит и загрузка снимка |

Методы: `bool ok() const` — сокращение для `status == Status::Ok`; `const std::string& text() const` — текст первого кода или пустая строка.

### Code

| Поле | Тип | Смысл |
| --- | --- | --- |
| `symbology` | `Symbology` | Символика кода — ровно один бит из маски (раздел «Символики») |
| `text` | `std::string` | Данные символа, байты как есть. Ведущий FNC1 (признак GS1) не передаётся, FNC1 внутри — разделитель GS, байт `0x1D`, а не его печатный вид |
| `box` | `Box` | Где символ найден на исходном кадре — прямоугольник по осям кадра |
| `corners` | `Quad` | Четыре угла символа там же, точнее бокса; смысл индексов свой у каждой символики (раздел «Геометрия») |
| `moduleCount` | `int` | DataMatrix: число модулей по стороне символа; 0 — неизвестно или не DataMatrix |
| `modulePx` | `float` | Линейные символики: ширина модуля, пикселей; 0 — неизвестно или не линейная символика |

### Кодировка текста

`Code::text` — байты, которые несёт символ; библиотека их не перекодирует и кодировку не угадывает.

- **DataMatrix.** Байты 0x00–0x7F — ASCII. Байты 0x80–0xFF (Upper Shift, режим Base 256) отдаются как есть. По стандарту DataMatrix такие байты по умолчанию — ISO 8859-1 (Latin-1), но многие генераторы кладут в Base 256 текст UTF-8, так что решать вам. ECI не поддерживается: переключение ECI не разбирается, и текст символа с ECI может содержать служебные байты; макросы 05/06 не разворачиваются в заголовок `[)>`, Structured Append не собирается — каждый символ читается отдельно. Коды «Честного знака» и GS1 DataMatrix ничего из этого не используют: в них только ASCII и разделитель `0x1D`.
- **Code 128 и GS1-128.** Наборы A, B и C и SHIFT; текст всегда 7-битный, 0x00–0x7F (набор A даёт и управляющие знаки 0x00–0x1F). FNC2, FNC3 и FNC4 не поддерживаются: символ с ними не читается — расширенных знаков Latin-1 через FNC4 в тексте не бывает.
- **EAN/UPC** — только цифры.

Для показа на экране, где нужен UTF-8: текст линейных символик — уже ASCII; текст DataMatrix проверьте на корректный UTF-8 и, если он не такой, переведите из Latin-1 (каждый байт 0x80–0xFF — один знак U+0080–U+00FF).

### Несколько кодов на кадре

```cpp
const unir::Scanner scanner(unir::Settings::multi(0));   // 0 — сколько найдётся
for (const unir::Code& c : scanner.scan(frame).codes)
    use(c.symbology, c.text, c.box);
```

С одним кодом (`maxCodes = 1`, умолчание) разбор останавливается на первом прочитанном — любой символики из маски. Если на кадре рядом DataMatrix и SSCC, а нужны оба, задавайте `multi()`; если нужен только один из них — сузьте маску (раздел «Символики»).

## Символики

Каждая символика — отдельный бит: `Settings::symbologies` — их маска, что искать, а `Code::symbology` — ровно один бит, что прочитано.

```cpp
enum class Symbology : uint32_t {
    None       = 0,
    DataMatrix = 1u << 0,  // ECC200, ГОСТ Р ИСО/МЭК 16022 (движок dmr)
    Code128    = 1u << 1,  // Code 128 без признака GS1 (движок ssccr)
    Gs1_128    = 1u << 2,  // GS1-128: Code 128 с FNC1 в начале (движок ssccr)
    Ean13      = 1u << 3,  // EAN/UPC — движок ssccr
    Ean8       = 1u << 4,
    UpcA       = 1u << 5,
    UpcE       = 1u << 6,
};

using Symbologies = uint32_t;   // маска символик

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;   // все символики этой сборки
```

```cpp
auto s = unir::Settings::stream();
s.symbologies = unir::Symbology::DataMatrix | unir::Symbology::Gs1_128;   // «Честный знак» и SSCC
const unir::Scanner scanner(s);
```

| Символика | Что читается |
| --- | --- |
| `DataMatrix` | DataMatrix ECC200 любой структуры — тем же движком и с теми же ответами, что у «Чтения DataMatrix» |
| `Gs1_128` | Code 128 с FNC1 в начале. Если в маске нет `Code128`, читается только SSCC (AI 00); вместе с `Code128` — и прочие GS1-128 |
| `Code128` | Code 128 без признака GS1. Приём строже, чем у SSCC: должны совпасть не меньше двух сканлайнов. Без `Gs1_128` в маске SSCC и прочие GS1-128 отбрасываются |
| `Ean13`, `Ean8`, `UpcA`, `UpcE` | Приём — согласие двух сканлайнов и свободные зоны. UPC-A — 12 цифр (EAN-13 с первой цифрой 0), UPC-E — 8 цифр (система, 6 цифр, контрольная) без разворота в UPC-A. Движок читает все четыре вместе, но отдаются только те, что в маске |

> **Умолчание — все символики сборки.** `Settings::symbologies` по умолчанию — `kAllSymbologies`, то есть всё, что умеет эта сборка. В следующих версиях добавятся QR и другие символики, и программа с умолчанием после обновления начнёт искать и их — а значит, и тратить на них время кадра. Если нужен фиксированный набор, задавайте маску явно.

### Имена и префиксы AIM

```cpp
// Короткое имя: "datamatrix", "code128", "gs1-128", "ean-13", "ean-8",
// "upc-a", "upc-e"; None — "none".
const char* nameOf(Symbology s);

// Идентификатор символики AIM (ISO/IEC 15424), без модификатора данных:
// "]d", "]C", "]E".
const char* aimPrefixOf(Symbology s);

// Маска из списка имён через запятую: "dm,128,gs1-128", "all".
// Неизвестное имя — false, mask не меняется.
bool parseSymbologies(const std::string& list, Symbologies& mask);
```

`parseSymbologies` понимает, без учёта регистра: `datamatrix` | `dm`, `code128` | `128`, `gs1-128` | `gs1` | `sscc`, `ean-13` | `ean13`, `ean-8` | `ean8`, `upc-a` | `upca`, `upc-e` | `upce`, `ean` — все четыре EAN/UPC, и `all`. Пустая строка — тоже неизвестное имя. Удобно, когда набор символик приходит из конфигурации или командной строки — тот же разбор у ключа `--sym` утилиты.

`aimPrefixOf` отдаёт идентификатор символики без модификатора: `]d` для DataMatrix, `]C` для Code 128 и GS1-128, `]E` для EAN/UPC. Признак GS1 (FNC1) у DataMatrix в этой версии наружу не отдаётся, поэтому модификатора вида `]d2` нет: GS1-структуру DataMatrix различайте по содержимому. Модификатор добавится, когда движки станут сообщать признак FNC1.

### Порядок разбора и порядок кодов

Движки зовутся по очереди, и порядок зависит от того, сколько кодов искать:

- **Один код** (`maxCodes = 1`): сначала быстрый проход линейного движка — SSCC и EAN/UPC, затем DataMatrix, затем прочий Code 128 (и прочий GS1-128) — он на кадре без штрихкода самый дорогой и поэтому последний. Первый прочитанный код и есть ответ: следующие движки уже не зовутся.
- **Несколько кодов** (`maxCodes` 0 или больше 1): сначала все линейные символики, затем DataMatrix. В `codes` — сперва линейные коды, потом DataMatrix; разбор кончается, как только набралось `maxCodes`.

Движки, которых нет в маске, в цепочку не попадают и времени не стоят. Прежде чем отдать кадр DataMatrix-движку, unir заливает уже прочитанные линейные коды вместе с их свободными зонами ровным тоном: штрихи не уводят его в перебор ложных кандидатов. Заливка строится по углам прочитанных кодов, чужого символа не задевает и на координаты в ответе не влияет.

## Кадр

### PixelFormat

```cpp
enum class PixelFormat {
    Gray8,  // один канал: яркость; для сканера самый дешёвый
    Bgr8,   // три канала, порядок OpenCV
    Rgb8,
    Bgra8,  // четыре канала, порядок OpenCV и Windows
    Rgba8,
};

constexpr int channelsOf(PixelFormat f);   // 1, 3 или 4
```

Для монохромной камеры кадр стоит передавать как `Gray8`, а не размноженным в три канала.

### ImageView

Вид на чужой буфер: библиотека его не копирует, не освобождает и не переживает — буфер обязан быть жив всё время вызова. Поля повторяют вид кадра у dmr и ssccr, поэтому движкам кадр уходит без копирования.

```cpp
struct ImageView {
    const uint8_t* data   = nullptr;
    int            width  = 0;
    int            height = 0;
    int            stride = 0;              // байт в строке; 0 — строки вплотную
    PixelFormat    format = PixelFormat::Gray8;

    bool empty()    const;
    int  channels() const;
    int  rowBytes() const;                  // stride с уже разобранным нулём
    const uint8_t* row(int y) const;
};
```

`stride = 0` значит «строки вплотную» (`width * каналов`), а не «ноль байт в строке»: кадр камеры почти никогда не плотный — строки выравнивают, а вырезка наследует длину строки оригинала. Пустой вид или `stride` меньше длины строки — `Status::InvalidImage`, до движков такой кадр не доходит.

### Image

Кадр, которым библиотека владеет сама, строки вплотную. Неявно приводится к `ImageView`, поэтому передаётся в `scan()` напрямую:

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

    void reset(int width, int height, PixelFormat format);  // буфер под размер, заполнен нулями
    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;   // неявно — для вызова scan(image)
};
```

### Чтение с диска

```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,          // снимок прочитан
    NotFound,    // файла нет
    Unreadable,  // файл есть, но это не читаемый снимок (или не успел за срок)
    TooLarge,    // размер из заголовка больше ImageLimits; пиксели не читались
};

enum class LoadAs {
    Color,  // PixelFormat::Bgr8
    Gray,   // PixelFormat::Gray8 — сканеру этого достаточно
};

LoadStatus loadImage(const std::string& path, Image& out, LoadAs as = LoadAs::Gray,
                     const ImageLimits& limits = ImageLimits{});
```

Снимок декодируется теми же декодерами, что у dmr, — с пределами размера и защитой от «бомб»: форматы **JPEG и PNG**, определяются по первым байтам файла, а не по расширению. Путь — в UTF-8 (в Windows годится и системная кодировка, как у `argv`: строка, не составляющая корректный UTF-8, читается в ней). По умолчанию снимок читается в полутон: сканеру цвет не нужен. Пределы сверяются с заголовком файла до выделения памяти; передавайте сюда те же, что в `Settings::imageLimits`. При отказе `out` не меняется.

```cpp
unir::Image frame;
switch (unir::loadImage("кадр.jpg", frame)) {
    case unir::LoadStatus::Ok:         break;
    case unir::LoadStatus::NotFound:   /* файла нет */ break;
    case unir::LoadStatus::Unreadable: /* файл есть, но это не снимок */ break;
    case unir::LoadStatus::TooLarge:   /* больше пределов */ break;
}
```

Отдельный `loadImage()` идёт без срока кадра. Когда снимок пришёл из недоверенного источника, удобнее `Scanner::scanFile()`: там загрузка и разбор — под одним сроком.

## Геометрия

```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` дробный: углы находятся точнее пикселя. `Box` — прямоугольник по осям кадра, `Quad` — четыре угла символа точнее, чем прямоугольником: на конвейере символ снят под углом.

```cpp
for (const unir::Point& p : c.corners.corner)
    drawTo(p.x, p.y);
```

> **Смысл индексов — свой у каждой символики.** Углы `Quad` всегда идут по кругу, но что стоит под каким индексом, зависит от символики:
>
> - **DataMatrix** — под индексом 3 стоит угол, который детектор принял за угол сплошной L-рамки. Для разметки поверх кадра `corner[3]` годится, для выводов о повороте содержимого символа — нет.
> - **Code 128 и GS1-128** — `corner[0]` на стороне старта, `corner[1]` на стороне стопа на том же краю штрихов, `corner[2]` и `corner[3]` — на противоположном краю. По ним видно направление чтения.
>
> Прежде чем читать смысл углов, смотрите на `Code::symbology`.

## Настройки

Начинать стоит с пресета, а не с отдельных полей — имя пресета уже говорит, под какой сценарий он настроен. Пресет задаёт режим DataMatrix-движка; линейный движок от режима не зависит.

| Пресет | Для чего | Чем отличается |
| --- | --- | --- |
| `Settings::stream()` | Поток кадров одной камеры — конвейер. Умолчание | `mode = Stream`: DataMatrix-движок копит подсказку размера символа между кадрами; один код |
| `Settings::single()` | Разовые, не связанные между собой снимки — папка, ручная проверка | `mode = Single`: подсказки нет, DataMatrix-движок перебирает все стандартные размеры, если размер не удалось измерить; один код |
| `Settings::multi(n)` | Несколько кодов на одном кадре | `mode = Multi`, `maxCodes = n` (по умолчанию 0 — сколько найдётся) |

```cpp
auto s = unir::Settings::stream();
s.symbologies = unir::Symbology::DataMatrix | unir::kEanUpc;
s.budgetMs    = 120.0;     // один срок на все символики; 0 — без ограничения
const unir::Scanner scanner(s);
```

### Все поля

```cpp
// unir/Settings.h
struct Settings {
    static Settings stream();                  // поток кадров одной камеры (умолчание)
    static Settings single();                  // кадры не связаны между собой
    static Settings multi(int maxCodes = 0);   // до maxCodes кодов на кадре (0 — все)

    Symbologies symbologies = kAllSymbologies; // какие символики искать
    int         maxCodes    = 1;               // 1 — до первого кода, N — до N, 0 — все
    double      budgetMs    = 0.0;             // срок кадра, мс, один на все символики; 0 — без
    double      timeoutMs   = 10000.0;         // предохранитель, мс; 0 — потолок 60 с
    ImageLimits imageLimits{};                 // 16384 по стороне, 100 000 000 пикселей

    enum class Mode { Stream, Single, Multi };
    Mode mode = Mode::Stream;                  // режим DataMatrix-движка
};
```

Пресеты меняют только `mode`, а `multi()` — ещё и `maxCodes`; остальные поля у всех трёх — умолчания из объявления.

| Поле | Умолчание | Смысл |
| --- | --- | --- |
| `symbologies` | `kAllSymbologies` | Какие символики искать — маска (раздел «Символики»). Умолчание — все символики сборки; в следующих версиях их станет больше |
| `maxCodes` | 1 | Сколько кодов искать: 1 — до первого прочитанного кода любой символики, N — до N, 0 — все |
| `budgetMs` | 0.0 | Предел времени на кадр, мс, — **один на все символики**: движки делят его по очереди, и тот, кому не осталось времени, не начинается вовсе (взводится `timedOut`). 0 — без предела, действует только `timeoutMs`. Срок мягкий: время кадра может превысить его на длительность идущей стадии. DataMatrix-движку бюджет передаётся как есть, и, как в самом dmr, заданный бюджет меняет и способ перебора, а не только обрывает его |
| `timeoutMs` | 10000.0 | Предохранитель, мс: тот же обрыв, что у `budgetMs` (из двух действует меньший), но способа работы движков не меняет. 0 и больше 60000 — потолок 60 с: дольше кадр не разбирается никогда. Защита от враждебных кадров, а не настройка задержки |
| `imageLimits` | 16384 по стороне, 100 000 000 пикселей | Наибольший кадр, который сканер берётся разбирать (`ImageLimits` из `unir/Image.h`; 0 — жёсткий потолок 65535 и 2^29). Кадр больше сразу получает `Status::ImageTooLarge`. `scanFile()` сверяет те же пределы с заголовком файла до декодирования. Защита, а не настройка качества |
| `mode` | `Mode::Stream` | Режим DataMatrix-движка: `Stream` — подсказка размера между кадрами, `Single` — одиночные кадры с перебором размеров, `Multi` — несколько кодов. Обычно выставляется пресетом |

Тонких настроек dmr (поворот, проверка GS1, источники кандидатов и прочие) unir наружу не выводит: DataMatrix-движок работает с пресетом dmr, выбранным по `mode`, и с вашими `maxCodes`, сроками и пределами кадра.

### Скорость: режим, маска, срок

Время кадра определяют три настройки, и все три стоит задавать явно, а не оставлять умолчания.

- **Режим.** С одним кодом (`stream()`, `single()`) разбор кончается на первом прочитанном коде, а первым идёт самый дешёвый проход — SSCC и EAN/UPC: кадр с одним штрихкодом читается быстро, и до DataMatrix дело не доходит. `multi(0)` остановиться не может: каждый движок маски проходит весь кадр до конца, линейный ищет все Code 128 и EAN/UPC, DataMatrix-движок перебирает всех своих кандидатов. На кадре с одним кодом это в разы, а на слабой машине — на порядок дольше режима одного кода. Если число кодов известно, задавайте `multi(N)`: разбор кончится, как только их наберётся N.
- **Маска.** Каждая символика вне маски — движок или проход, которого нет. Дороже всего обходятся «ничего не найдено» на кадре без кода: прочий `Code128` примеряется ко всему, что похоже на штрихи, а DataMatrix-движок уходит в глубокий перебор. SSCC читается и без `Code128` — по одному `Gs1_128`. Маска только из нужных символик заметно быстрее «всех», маска из одной DataMatrix — тем более.
- **Срок.** `budgetMs` — один срок на кадр для всех символик. Движки тратят его по очереди; кончился — идущий движок обрывается на ближайшей стадии, следующие не начинаются, в результате взводится `timedOut`. Прочитанное до срока остаётся в `codes`. Без `budgetMs` кадр ограничен только предохранителем `timeoutMs` (10 с), и в `multi(0)` на трудном кадре это ощутимо.

Практически: на конвейере срок — интервал между кадрами за вычетом своей обработки; для разового снимка, ответа на который ждёт человек, в `multi()` — столько, сколько он готов ждать, например `budgetMs = 3000`. У `scanFile()` в этот же срок входит загрузка файла.

## Версия библиотеки

```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();   // версии движков внутри сборки: "dmr 1.0.12, ssccr 0.1.0"
}
```

Макросы говорят про заголовок, с которым скомпилировались; `unir::version()` — про библиотеку, с которой слинковались. Разойтись они могут: библиотека ставится в префикс и живёт там дольше, чем помнит собиравший. `engineVersions()` нужен для обращений в поддержку и сверки с «Чтением DataMatrix»: по нему видно, какой ревизией dmr и ssccr собрана поставка.

```cpp
#if UNIR_VERSION >= UNIR_VERSION_AT(1, 1, 0)
    // код под заголовок 1.1.0 и новее
#endif
```

## Лицензия

Библиотека читает коды, пока действует пробный период (30 дней с первого запуска на машине, без сети и регистрации) или лицензия. Иначе `Scanner::scan()` и `scanFile()` сразу отвечают `Status::Unlicensed`: ни один движок не зовётся, файл снимка не открывается. Лицензия привязана к оборудованию (материнская плата, процессор, системный диск), а не к установке ОС: переустановка Windows и замена диска или процессора её не отбирают, замена материнской платы — отбирает.

Пробный период и лицензия unir — свои, независимые от «Чтения DataMatrix» и «Создания DataMatrix» на той же машине: сервер лицензий тот же, но продукт другой, и лицензия одного продукта не открывает другой.

```cpp
#include <unir/License.h>

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

unir::LicenseResult r = unir::license::activate(keyFromUser);   // «UNIR-XXXXX-XXXXX-XXXXX-XXXXX»
if (!r.ok) show(r.message);
```

### Файл ключа unir.key

Основной способ активации — вместо вызова `activate()` из своей программы положить ключ в текстовый файл `unir.key`. Имя своё, не `license.key`, как у «Чтения DataMatrix»: библиотеки разных продуктов могут лежать в одном каталоге, а ключи у них разные. Библиотека ищет файл, берётся первый найденный:

1. рядом с модулем, где живёт библиотека: `unir_c.dll` / `libunir_c.so` для C ABI, сама программа — для статической `libunir.a` и утилиты `unir`;
2. рядом с исполняемым файлом программы.

Текущий каталог и переменные окружения не просматриваются. Формат: ключ — первая строка, которая не пуста и не комментарий `#`, в том виде, в каком выдан — `UNIR-XXXXX-XXXXX-XXXXX-XXXXX` (регистр, пробелы и дефисы не важны; BOM UTF-8 допустим; файлы больше 4 КБ не читаются). Ключ активируется по сети сам, вызывать ничего не нужно: при создании первого `Scanner` или первом вызове функции `license::` — этот вызов один раз ждёт ответа сервера. Не вышло — причина в `status().message`; без сети попытка повторяется раз в час, при отказе сервера — раз в сутки, в том числе между запусками. Файл, положенный при работающей программе, подхватывается в течение часа. Действующую лицензию файл с другим ключом не заменяет; ключ, отвязанный `deactivate()`, из файла снова не активируется. Какой файл библиотека нашла, скажет `license::keyFile()`.

### Где хранится состояние пробного периода и лицензии

Windows: `%ProgramData%\unir\license.dat`, `%LOCALAPPDATA%\unir\license.dat` и значение `state` в разделе реестра `HKCU\Software\unir`. Linux: `/var/lib/unir/license.dat` и `~/.config/unir/license.dat` пользователя, от которого работает процесс (по базе пользователей, а не по `$HOME`). Копий несколько, берётся самое раннее начало пробного периода. У контейнера нет ни данных DMI, ни своего диска, поэтому лицензию к нему не привязать — запускайте программу на хосте или в виртуальной машине.

На Linux общую копию в `/var/lib/unir` может создать только root: под обычным пользователем каталог не создаётся, и эта копия молча пропускается — состояние живёт в `~/.config/unir`. Ошибкой это не считается. Пробный период заводится при первом обращении к библиотеке на машине — первом `Scanner` или первом `license::status()` — и записывается сразу, а не при выходе из программы.

### LicenseState

| Значение | Смысл |
| --- | --- |
| `Trial` | Идёт пробный период |
| `Active` | Лицензия действует |
| `RefreshDue` | Действует, но связи с сервером давно не было; автосверка повторяет попытки раз в час |
| `TrialExpired` | Пробный период кончился, лицензии нет |
| `Expired` | Оплаченный срок лицензии кончился; продление в личном кабинете автосверка подхватит сама |
| `NetworkRequired` | Без связи с сервером дальше нельзя; как только сервер ответит, автосверка вернёт лицензию |
| `WrongMachine` | Лицензия выдана другой машине |
| `Invalid` | Запись лицензии не проходит проверку подписи |

### 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 или RefreshDue

    int64_t validUntil   = 0;        // unix-секунды UTC
    int64_t refreshAfter = 0;        // 0 — не устаревает
    int64_t offlineUntil = 0;        // 0 — без ограничения
    int     daysLeft     = 0;

    std::string licenseId;
    std::string activationId;
    std::string offlineMode;         // limited | extended | full; пусто без лицензии
    bool        clockRollback = false;
    std::string message;             // состояние словами, по-русски
};

struct LicenseResult {
    bool          ok = false;
    std::string   error;             // код отказа: LICENSE_NOT_FOUND, KEY_INVALID, NETWORK, ...
    std::string   message;
    LicenseStatus status;            // состояние после операции
};
```

Функции «`LicenseState` → строка» в C++ API нет: для пользователя есть `message`, а машинный код состояния (`trial`, `active`, …) отдают только JSON утилиты, C ABI и Python. Если он нужен в C++ — короткий `switch` с теми же именами:

```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";
}
```

| Поле | Смысл |
| --- | --- |
| `state` | `LicenseState` |
| `canScan()` | Читает ли сейчас библиотека — true при `Trial`, `Active` или `RefreshDue` |
| `validUntil` | Конец пробного периода или оплаченного срока, unix-секунды UTC |
| `refreshAfter` | С какого момента токен считается устаревшим и лицензия переходит в `RefreshDue`; 0 — не устаревает (пробный период, режим `full`) |
| `offlineUntil` | Дольше этого без связи с сервером лицензия не работает; 0 — без ограничения (режим `full`) |
| `daysLeft` | Целых суток до ближайшей из границ выше; 0 — граница пройдена |
| `licenseId` / `activationId` | Идентификаторы лицензии и конкретной активации |
| `offlineMode` | `limited \| extended \| full`; пусто без лицензии |
| `clockRollback` | Системные часы отстают от времени, которое машина уже видела — откат часов срок не продлевает |
| `message` | Состояние словами, для показа пользователю |

Функции, которые что-то делают с лицензией, возвращают `LicenseResult`: `ok`, код отказа `error` (ответ сервера как есть — `LICENSE_NOT_FOUND`, `ACTIVATION_LIMIT_REACHED`, `LICENSE_REVOKED` и другие — либо свой: `KEY_INVALID`, `NETWORK`, `WRONG_PRODUCT` и другие), `message` словами и `status` — состояние уже после операции.

### Активация: онлайн и офлайн

Онлайн — ключ из личного кабинета, машина сама идёт на сервер лицензий:

```cpp
unir::LicenseResult r = unir::license::activate("UNIR-XXXXX-XXXXX-XXXXX-XXXXX");
```

Офлайн — для машины без интернета: файл запроса переносится на машину с сетью и загружается в личный кабинет, в ответ приходит `license.lic`:

```cpp
unir::license::writeActivationRequest("request.json");   // → загрузить в кабинет, получить license.lic
unir::license::importLicenseFile("license.lic");          // тем же вызовом продлевается: новый файл заменяет старый
```

Активированная по сети лицензия раз в сутки сама сверяется с сервером в фоновом потоке — обновляет токен (режимам `limited` и `extended` это нужно, чтобы работать дальше), приносит отзыв и продление срока. Вызывать для этого ничего не нужно, а чтение кадра сеть не ждёт никогда; сервер недоступен — повтор через час, лицензия работает, пока не выйдет срок работы без связи.

### Переезд и остальные функции

```cpp
namespace unir::license {
    void configure(const LicenseOptions& options);   // до первого Scanner; можно не звать
    LicenseStatus status();
    LicenseResult activate(const std::string& licenseKey);
    LicenseResult refresh();                                    // сверить сейчас, не дожидаясь суток
    LicenseResult deactivate(const std::string& reason = {});    // расписка уходит на сервер
    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();                           // для обращений в поддержку
    std::filesystem::path keyFile();                            // найденный unir.key; пусто — нет
}

struct LicenseOptions {
    std::string serverUrl;    // пусто — вшитый при сборке
    std::string appVersion;   // версия вашей программы — видна в кабинете рядом с машиной
    int  timeoutMs = 15000;   // предел ожидания одного запроса к серверу
    bool autoCheck = true;    // суточная автосверка
};
```

`configure()` нужно звать до создания первого `Scanner`: активация ключа из `unir.key` идёт при первом обращении к библиотеке и берёт сервер и `appVersion` уже отсюда. Подменять сервер безопасно: ответы проверяются подписью по ключам, вшитым в библиотеку. Выключать `autoCheck` стоит, только если программа по своим правилам не должна выходить в сеть сама; тогда лицензии `limited` и `extended` продлевает её собственный вызов `refresh()`. У лицензии, принятой из файла, ключа нет, и `refresh()` ей не поможет — её продлевают новым `license.lic`.

Переезд на другую машину — `deactivate()` на старой: место освобождается на сервере распиской, подписанной секретом этой машины; без связи лицензия остаётся на месте. Без сети — `deactivateToFile()`: лицензия удаляется с машины сразу, а расписка ложится в файл, отправить его можно с любой машины. Все функции потокобезопасны; функции с сетью блокируют вызывающего до ответа или до истечения `LicenseOptions::timeoutMs`.

Каждый ответ сервера лицензий привязан к своему запросу: библиотека отправляет случайное одноразовое число, сервер возвращает его в подписанном ответе, и записанный раньше ответ повторно не примется. Расписка о сдаче места тоже одноразовая, поэтому расписку из восстановленной копии старого состояния машины сервер не примет. У лицензий с полной автономией (`full`) сдача места расходует лимит переносов; когда он исчерпан, перенос делает поддержка. Самостоятельно освободить место по лицензии можно не больше двух раз за 30 дней в режиме `limited` и не больше одного раза — в `extended`; дальше перенос делает поддержка.

> **Ключ активации.** Ключ активации выдаётся в личном кабинете после оформления тарифа в [каталоге](https://libscanner.com/).

## C ABI

Та же библиотека за плоским интерфейсом на C99 — заголовок `unir/unir_c.h`, библиотека `unir_c.dll` / `libunir_c.so` (как подключить — «Подключение»). Для C, MSVC и любого языка с вызовом функций C (C#, Go, Delphi, Rust…). Правила границы:

- Через неё ходят только простые типы C: целые, `double`, указатели на буферы известного размера, перечисления как `int` и непрозрачные дескрипторы. Исключение C++ границу не пересекает — внутренний сбой превращается в `NULL` или код ошибки.
- Владение: всё, что функция *возвращает* (дескриптор, строка), принадлежит вызывающему и освобождается своей функцией (`unir_image_free`, `unir_scan_result_free`, `unir_free_string`; сканер — `unir_scanner_destroy`). Указатели из функций доступа (`unir_scan_result_code_text` и подобных) живут не дольше объекта, из которого взяты, и отдельно не освобождаются. Строку `unir_symbology_name` не освобождают — она статическая.
- `unir_settings_t` начинается с `struct_size` и метки `abi_tag`: их выставляет вызывающий **до** пресета — в C проще всего `unir_settings_t s = UNIR_SETTINGS_INIT;`, в C++ — `unir_settings_prepare(&s)`. Пресет пишет не дальше `struct_size`; поля, которых вызывающий не знает (более новая библиотека), остаются по умолчанию. Чужая метка или размер вне допустимого — отказ: пресет вернёт 0, `unir_scanner_create` — `NULL`.
- Символики — биты маски `UNIR_SYM_*`, те же значения, что у `unir::Symbology` (раздел «Символики»); у прочитанного кода — ровно один бит (`unir_scan_result_code_symbology`), имя — `unir_symbology_name`. `UNIR_SYM_ALL` — все символики *этой* сборки: в следующих версиях в неё войдут новые (QR и другие), поэтому для неизменного набора задавайте маску явно.
- Прочитанный текст хранит разделитель GS байтом `0x1D` и может содержать нулевой байт — длину брать у `unir_scan_result_code_text_len`, а не `strlen()`.
- Пути — в UTF-8. В Windows годится и системная кодировка, как у `argv`: строка, не составляющая корректный UTF-8, читается в ней. Имя, которого нет в кодовой странице Windows, передавайте в UTF-8.
- Статусы — те же, что `unir::Status` (раздел «Результат чтения»), плюс `UNIR_STATUS_INTERNAL_ERROR = -1` на сбой самого слоя C. `unir_scanner_scan` и `unir_scanner_scan_file` возвращают `NULL` только при внутренней ошибке; «кода нет» — это результат со статусом `UNIR_STATUS_NOT_FOUND`. Снимок, который не загрузился, — `UNIR_STATUS_IMAGE_TOO_LARGE` или `UNIR_STATUS_INVALID`, подробности — в `*load_status`.
- Один сканер не вызывают из нескольких потоков одновременно — по сканеру на поток.
- Функции лицензии возвращают тот же JSON, что `unir --license … --json` (раздел «Командная строка»): один контракт для утилиты, C ABI и Python.

### Полный пример: все коды со снимка

```cpp
#include <unir/unir_c.h>
#include <stdio.h>

/* Все коды со снимка: 0 — прочитаны, 1 — нет кода, 2 — ошибка. */
int main(int argc, char** argv) {
    if (argc < 2) return 2;

    unir_settings_t settings = UNIR_SETTINGS_INIT;   /* размер и метка — до пресета */
    unir_settings_multi(&settings, 0);               /* все коды на снимке */
    settings.symbologies = UNIR_SYM_ALL;             /* или, например, UNIR_SYM_DATAMATRIX | UNIR_SYM_EAN_UPC */
    settings.budget_ms   = 3000;                     /* один срок на загрузку и все символики */

    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 — внутренняя ошибка */
        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)));
                /* в тексте бывают 0x1D (GS) и даже 0x00 — длина, а не 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);   /* нет файла, не снимок, велик */
        }
        unir_scan_result_free(r);
    }
    unir_scanner_destroy(scanner);
    return rc;
}
```

В Linux: `cc main.c -o read $(pkg-config --cflags --libs unir_c)`, запуск — `LD_LIBRARY_PATH=/куда/распакован/lib ./read этикетка.png`. Печатает по строке на код: символику, табуляцию и текст — у DataMatrix «Честного знака» с настоящими байтами `0x1D` между элементами.

### Справочник

Все объявления `unir_c.h` (ABI 1.0, библиотека 1.0.1); комментарии сокращены — полные в самом заголовке.

```cpp
/* ── версии ── */
#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);          /* версия библиотеки, "1.0.1" */
size_t unir_engine_versions(char* buf, size_t buf_size);  /* "dmr 1.0.12, ssccr 0.1.0" */

/* ── символики: биты маски ── */
#define UNIR_SYM_NONE        0x00u
#define UNIR_SYM_DATAMATRIX  0x01u
#define UNIR_SYM_CODE128     0x02u   /* Code 128 без признака GS1 */
#define UNIR_SYM_GS1_128     0x04u   /* без UNIR_SYM_CODE128 в маске — только SSCC (AI 00) */
#define UNIR_SYM_EAN13       0x08u
#define UNIR_SYM_EAN8        0x10u
#define UNIR_SYM_UPCA        0x20u   /* 12 цифр */
#define UNIR_SYM_UPCE        0x40u   /* 8 цифр, без разворота в 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";
                                                            * маска или чужой бит — "unknown" */
int unir_parse_symbologies(const char* list, uint32_t* mask);   /* "dm,128,ean", "all"; 1 — готово,
                                                                  * 0 — неизвестное имя */

/* ── кадр ── */
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;     /* буфер вызывающего, жив весь вызов scan */
    int                 width;
    int                 height;
    int                 stride;   /* байт на строку; 0 — плотно */
    unir_pixel_format_t format;
} unir_image_view_t;

typedef struct unir_image unir_image_t;                 /* непрозрачный, буфер библиотеки */
typedef struct unir_settings unir_settings_t;           /* определена ниже */
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 и PNG; GRAY — GRAY8, COLOR — BGR8; *out — NULL при отказе */
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 — по умолчанию */
                                      unir_image_t** out);
unir_image_view_t  unir_image_view(const unir_image_t* image);
void               unir_image_free(unir_image_t* image);

/* ── настройки: те же поля и умолчания, что 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), ставит вызывающий ПЕРВЫМ */
    uint32_t abi_tag;            /* UNIR_SETTINGS_TAG, тоже до пресета */
    uint32_t symbologies;        /* маска UNIR_SYM_*; в пресетах UNIR_SYM_ALL */
    int      max_codes;          /* 1 — до первого кода любой символики, N — до N, 0 — все */
    int      mode;               /* unir_mode_t */
    double   budget_ms;          /* ОДИН срок на все символики; 0 — без предела */
    double   timeout_ms;         /* предохранитель, в пресетах 10000; 0 — потолок 60 с */
    int      max_image_side;     /* 0 — потолок 65535; в пресетах 16384 */
    int64_t  max_image_pixels;   /* 0 — потолок 2^29; в пресетах 100 000 000 */
};
#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);   /* обнуляет, ставит размер и метку */
int unir_settings_stream(unir_settings_t* out);         /* 1 — заполнено; 0 — NULL, чужая */
int unir_settings_single(unir_settings_t* out);         /*   метка или размер         */
int unir_settings_multi(unir_settings_t* out, int max_codes);   /* 0 — все коды */

/* ── сканер ── */
typedef struct unir_scanner unir_scanner_t;             /* непрозрачный; по сканеру на поток */
unir_scanner_t* unir_scanner_create(const unir_settings_t* settings);   /* NULL — stream() */
void            unir_scanner_destroy(unir_scanner_t* scanner);

/* ── результат ── */
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;     /* непрозрачный */
unir_scan_result_t* unir_scanner_scan(unir_scanner_t* scanner,
                                      const unir_image_view_t* frame);   /* NULL — внутренняя ошибка */
/* снимок с диска и коды на нём под одним сроком сканера; итог загрузки —
 * в *load_status (NULL — не нужен) */
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);   /* один бит */
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);     /* линейные */

/* ── лицензия: JSON как у "unir --license … --json"; освобождать unir_free_string, NULL — внутренняя ошибка ── */
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 может быть 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);
```

## Командная строка

В поставку вместе с библиотекой входит утилита `unir` — для быстрой проверки без своего кода: разовый снимок, список путей, папка или поток для внешнего сервиса. Режимы и форматы — как у утилиты `dmr`, с колонкой символики.

```
unir <снимок>           [ключи]
unir --batch  <список>  [ключи]   список путей, по одному на строку
unir --folder <каталог> [ключи]   все снимки каталога (без вложенных)
unir --serve            [ключи]   путь на строку из stdin
unir --license <подкоманда>       лицензия
unir --version                    версии unir и движков
```

| Ключ | Смысл |
| --- | --- |
| `--sym СПИСОК` | Символики через запятую, имена — как у `parseSymbologies`: `dm`, `128`, `gs1` (`sscc`), `ean` (`ean-13`, `ean-8`, `upc-a`, `upc-e`), `all` — умолчание |
| `--codes N` | Сколько кодов искать: 1 — умолчание, 0 — все; любое значение, кроме 1, включает пресет `multi(N)` |
| `--single` | Снимки не связаны между собой — пресет `single()`; умолчание — поток, `stream()` |
| `--budget-ms N` | Предел времени на снимок, мс, один на все символики; 0 — без предела |
| `--timeout-ms N` | Предохранитель, мс (10000); 0 — потолок 60 с |
| `--max-side N` / `--max-pixels N` | Наибольшая сторона (16384) и площадь (100000000) снимка |
| `--color` | Читать снимок в цвете; по умолчанию — в полутон |
| `--corners` | В пакетных режимах — углы символа шестой колонкой |
| `--root D` | В `--serve`: читать только снимки внутри каталога D |

### Разовый снимок

```
$ unir этикетка.jpg
обработка: 24.9 мс
код: datamatrix 0104627191145677215n5Gr,pHTGIU&<GS>93CuXU
```

Сообщения утилиты — на языке системы (на любой не русской — по-английски: `processing`, `code`); ключ `--lang ru|en` или переменная `LIBSCANNER_LANG` задают его явно. Строки `--batch`/`--serve` и вывод `--json` от языка не зависят. Та же переменная задаёт язык `status().message` и сообщений об ошибках в самой библиотеке.

По строке `код: <символика> <текст>` на каждый код, символика — по `nameOf()`. В тексте GS показан как `<GS>`, TAB — как `<TAB>`, переводы строк — пробелами. Если срок оборвал перебор, следом идёт строка `бюджет: исчерпан, перебор оборван`. Ошибки — в stderr. Результат — код возврата. Номера те же, что у `unir::Status`, с одним отличием: снимок, который не загрузился (нет файла, не JPEG/PNG, декодирование не уложилось в срок), даёт 1, а не 5 (`InvalidImage`), как вернул бы `scanFile()`; слишком большой снимок — 4, как и `ImageTooLarge`. Срок, оборвавший разбор уже загруженного снимка, кода не меняет: это 1 или 2 (или 0, если что-то успело прочитаться) и строка про бюджет. Неверные ключи командной строки — тоже 1.

| Код | Смысл |
| --- | --- |
| 0 | Прочитан хотя бы один код (и когда срок оборвал перебор, но коды есть) |
| 1 | Код не найден; также нет файла, не JPEG/PNG, вышел срок декодирования, неверные ключи; в `--serve` — путь вне каталога `--root` |
| 2 | Найден кандидат, но не прочитан — это не значит, что код на снимке есть (раздел «Что на самом деле значит NotDecoded») |
| 3 | Нет пробного периода и лицензии |
| 4 | Снимок больше предела |
| 5 | Противоречивый кадр; на снимках с диска утилита его не отдаёт — незагруженный файл даёт 1 |
| 6 | Внутренняя ошибка; в `--serve` — ещё и неверный список символик в запросе |

### Пакетный режим и поток

`--batch` и `--folder` — один процесс на весь заранее известный набор снимков, результат построчно на stdout, по строке на код:

```
путь<TAB>код возврата<TAB>мс<TAB>текст<TAB>символика[<TAB>углы]
```

Первые четыре колонки — те же, что у утилиты `dmr`, так что разбор её вывода подходит и здесь. Снимок без кода — одна строка с пустым текстом и без колонки символики; во второй колонке — коды из таблицы выше. `--corners` добавляет шестую колонку `x0,y0 x1,y1 x2,y2 x3,y3`. Сводка — в stderr. Без лицензии прогон останавливается на первом снимке с кодом 3: остальные ответили бы так же.

`--serve` — тот же формат строки, но снимки не известны заранее: путь приходит по одному, по строке из stdin, и обрабатывается сразу же — процесс держит сканер живым между запросами внешнего сервиса, не платя за инициализацию на каждый. После строк каждого снимка идёт строка `#END#`, и вывод сбрасывается сразу — по ней видно, что ответ закончен, даже когда кодов несколько. Строка запроса — путь или `путь<TAB>sym=СПИСОК`: символики этого снимка в том же виде, что у `--sym`; на каждый набор заводится свой сканер со своей подсказкой DataMatrix. Без лицензии служба отвечает строкой снимка с кодом 3, `#END#` и выходит с кодом 3. Процесс завершается по концу ввода.

```
$ mkfifo queue
$ unir --serve --codes 0 < queue &
$ printf '/путь/к/снимку.jpg\tsym=dm,gs1\n' > queue
/путь/к/снимку.jpg	0	91.24	0104627191145677215n5Gr,pHTGIU&<GS>93CuXU	datamatrix
#END#
```

### Лицензия из командной строки

```
unir --license status                     [--json]
unir --license activate <ключ>            [--server URL] [--json]
unir --license refresh                    [--server URL] [--json]
unir --license request  <request.json>
unir --license import   <license.lic>
unir --license deactivate [<файл>]        [--server URL] [--json]
unir --license fingerprint                [--json]
```

`request` пишет файл запроса офлайн-активации, `import` принимает `license.lic` (и продление тоже), `deactivate` без файла отвязывает машину по сети, с файлом — пишет расписку для отправки с другой машины. Коды возврата: `status` — 0, если unir читает, и 3, если нет; остальные подкоманды — 0 (сделано) или 1 (не удалось).

Флаг `--json` даёт тот же результат одной JSON-строкой на stdout — для скриптов и админ-панелей. Поля, их порядок и коды возврата — точно как у `dmr --license … --json`, поэтому сервис, который уже разбирает вывод dmr, может звать `unir` вместо него:

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

Операции (`activate`, `refresh`, `deactivate`) отвечают объектом `{"ok":…,"error":…,"message":…,"status":{…}}`, где `status` — состояние в том же виде, что выше; `fingerprint` — `{"fingerprint":"…"}`. Поле `state` — устойчивый машиночитаемый код (`trial`, `active`, `refresh_due`, `trial_expired`, `expired`, `network_required`, `wrong_machine`, `invalid`) — часть формата вывода, не переводится и не меняется словами.
