Обзор
Библиотека читает коды DataMatrix (ECC200) с кадров производственной камеры — например, коробки с маркировкой «Честного знака», где символ занимает на кадре около сотни пикселей. Ридер рассчитан на низкое разрешение съёмки — порядка двух пикселей на модуль символа — и проверяет каждую гипотезу геометрии кодовой коррекцией Рида-Соломона. Точность и скорость чтения — вверху страницы; ниже — только устройство интерфейса.
Библиотека статическая, публичный интерфейс — на C++20, восемь заголовков в include/dmr/. Кадр передаётся видом на чужой буфер (dmr::ImageView) — без копирования, из уже имеющегося буфера камеры или снимка.
По умолчанию сканер читает DataMatrix любой структуры. Режим requireGs1 (раздел «Настройки») дополнительно проверяет формат содержимого — AI 01 (GTIN, 14 цифр со сходящейся контрольной суммой), затем AI 21 — и отбраковывает символы, не подходящие под структуру GS1.
Подключение
Библиотека приходит уже собранной — собирать её не нужно, собирается только ваша программа. От неё требуется одно: компилятор C++20. API на C++, в результате возвращаются std::string и std::vector, поэтому компилятор и его стандартная библиотека должны быть теми же, что и у поставки.
В поставке семь публичных заголовков, статическая библиотека, файл для pkg-config и лицензии — и ничего сверх. Состав одинаковый на обеих ОС, различаются только команды подключения:
include\dmr\{Dmr,Diagnostics,Geometry,Image,Scanner,Settings,Version}.h
lib\libdmr.a
lib\pkgconfig\dmr.pc
share\doc\dmr\licenses\Поставка для Windows собрана MinGW-w64 GCC — libdmr.a в его формате и с его ABI. MSVC такой статический архив не слинкует; вашей программе нужен тот же тулчейн (например, MSYS2/MinGW или CLion с профилем MinGW).
Тулчейн бесплатный: MSYS2 — установщик с сайта, затем в его консоли MINGW64 (поставка собрана именно в этом окружении, не в UCRT64):
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconfПути включения и строка линковки берутся из dmr.pc — под MSYS2/MinGW pkg-config уже есть в системе:
pkg-config --static --cflags --libs dmrЕсли поставка распакована не в системный префикс — путь к ней явно, через переменную окружения (PowerShell):
$env:PKG_CONFIG_PATH = "C:\куда\распакован\lib\pkgconfig"
pkg-config --static --cflags --libs dmrПоставка — одна статическая библиотека, а значит вызывающему нужны и те библиотеки, на которых стоит она сама. Без --static их в командной строке не окажется, и линковка упадёт на неразрешённых символах.
В meson — то же самое через зависимость по имени пакета, одинаково на обеих ОС:
dmr_dep = dependency('dmr', required: true, static: true)
executable('мояпрограмма', 'main.cpp', dependencies: [dmr_dep])Под Windows дополнительно требуются системные winhttp, crypt32 и shell32 (используются проверкой лицензии) — они уже прописаны в Libs.private пакета, отдельно подключать не нужно.
Быстрый старт
Минимальная программа: снимок с диска, чтение, вывод текста. Всё, что нужно, — один заголовок:
#include <dmr/Dmr.h>
#include <iostream>
int main() {
dmr::Image frame;
if (dmr::loadImage("кадр.jpg", frame) != dmr::LoadStatus::Ok) {
std::cerr << "не удалось прочитать файл\n";
return 1;
}
dmr::Scanner scanner; // поток кадров одной этикетки
const dmr::ScanResult r = scanner.scan(frame); // Image неявно приводится к ImageView
if (r.ok()) {
std::cout << r.text() << "\n";
} else if (r.status == dmr::Status::NotFound) {
std::cout << "символа на кадре нет\n";
} else {
std::cout << "символ есть, прочитать не вышло\n";
}
}Для кадра из своего конвейера захвата (SDK камеры, буфер OpenCV, Windows-битмап) диск не участвует — собирается вид на уже имеющийся буфер, без копирования:
const dmr::ImageView view{ m.data, m.cols, m.rows, (int)m.step, dmr::PixelFormat::Bgr8 };
const dmr::ScanResult r = scanner.scan(view);Заголовки
Публичных заголовков семь, все под include/dmr/; восьмой, dmr/Dmr.h, включает их все разом.
| Заголовок | Что в нём |
|---|---|
dmr/Dmr.h | Агрегат — включает всё нижеперечисленное |
dmr/Image.h | ImageView, Image, PixelFormat, loadImage, пути |
dmr/Scanner.h | Scanner, ScanResult, Code, Status |
dmr/Settings.h | Настройки и пресеты сканера |
dmr/Geometry.h | Point, Box, Quad — где на кадре расположен код |
dmr/Diagnostics.h | explain(), приёмник журнала (LogSink) |
dmr/License.h | Проверка и активация лицензии, пространство dmr::license |
dmr/Version.h | Версия заголовка (макросы) и собранной библиотеки (dmr::version()) |
Заголовки не зависят друг от друга сверх необходимого: Settings.h и Scanner.h тянут за собой <functional> и <vector>, а тому, кто только собирает вид на кадр, достаточно одного Image.h.
Scanner
Scanner — объект, а не функция, и создавать его на каждый кадр не нужно: подсказка размера символа (Settings::mcHintRun) копится по кадрам потока и обязана их пережить, а число потоков пула — настройка процесса. При этом scan() объявлен const и не складывает в объект ничего, кроме этой подсказки: результат зависит от кадра, а не от истории чтений. Кандидаты внутри перебираются параллельно — свой пул потоков поверх scan() заводить не нужно.
class Scanner {
public:
explicit Scanner(Settings s = Settings::stream());
~Scanner();
Scanner(Scanner&&) noexcept;
Scanner& operator=(Scanner&&) noexcept;
Scanner(const Scanner&) = delete;
Scanner& operator=(const Scanner&) = delete;
// Прочитать коды на кадре.
// frame кадр целиком, цветной или полутоновый
// moduleCount сторона символа в модулях; 0 — определять самому
ScanResult scan(ImageView frame, int moduleCount = 0) const;
// Размер символа, подсказанный предыдущими кадрами; 0 — подсказки нет.
int moduleCountHint() const;
// Забыть подсказку.
void resetStream();
const Settings& settings() const;
};
// Разовое чтение без объекта: проба, скрипт, один файл. Подсказка размера
// по потоку кадров здесь не работает — копить её нечему.
ScanResult scan(ImageView frame, const Settings& s = Settings::single());Когда этикетка на конвейере сменилась, подсказку стоит сбросить явно, не дожидаясь, пока она погаснет сама:
scanner.resetStream();Scanner можно перемещать (Scanner&&), но не копировать — конструктор и оператор копирования удалены явно.
Результат чтения
Status
enum class Status {
Ok = 0, // прочитан хотя бы один код
NotFound = 1, // символ на кадре не найден
NotDecoded = 2, // символ найден, но не прочитан
Unlicensed = 3, // кадр не разбирался: нет пробного периода и лицензии
};При Status::Unlicensed кадр не разбирался вовсе — причину даёт dmr::license::status() (раздел «Лицензия»).
ScanResult
| Поле | Тип | Смысл |
|---|---|---|
status | Status | Чем кончился разбор кадра |
codes | std::vector<Code> | Прочитанные коды, по одному на символ, в порядке чтения: сверху вниз, при равном верхнем крае — слева направо. Пуст, если status не Ok. При Settings::maxCodes = 1 длина не больше единицы |
timedOut | bool | Перебор оборвался по Settings::budgetMs. Взводится, только когда бюджет и правда что-то отсёк — успех при взведённом флаге возможен: код прочитан, но часть гипотез не проверена |
elapsedMs | double | Сколько занял разбор кадра, мс, без учёта загрузки снимка с диска |
details | std::shared_ptr<const Details> | Разбор для диагностики; пусто, если не запрошен через Settings::collectDetails |
Методы: bool ok() const — сокращение для status == Status::Ok; const std::string& text() const — текст первого кода или пустая строка, чтобы не писать codes.empty() ? "" : codes[0].text на каждом вызове.
Code
| Поле | Тип | Смысл |
|---|---|---|
text | std::string | Декодированная строка. Байты как есть: разделители GS остаются байтом 0x1D, а не подставляются печатным видом |
box | Box | Где символ найден на исходном кадре — прямоугольник кандидата |
corners | Quad | Четыре угла символа там же, точнее бокса (раздел «Геометрия») |
moduleCount | int | Сторона символа в модулях |
box нужен не для красоты: два кандидата с одинаковой строкой — это один символ, увиденный дважды, если боксы пересекаются, и две одинаковые этикетки, если нет.
Несколько кодов на кадре
const dmr::Scanner scanner(dmr::Settings::multi(0)); // 0 — сколько найдётся
for (const dmr::Code& c : scanner.scan(frame).codes)
use(c.text, c.box, c.moduleCount);Кадр
PixelFormat
enum class PixelFormat {
Gray8, // один канал: яркость; для сканера самый дешёвый
Bgr8, // три канала, порядок OpenCV
Rgb8, // три канала, порядок большинства остальных
Bgra8, // четыре канала, порядок OpenCV и Windows
Rgba8, // четыре канала
};Сканер работает с яркостью и цветной кадр первым делом переводит в неё. Для монохромной камеры кадр стоит передавать как Gray8, а не размноженным в три канала — ответ тот же, а кадр обрабатывается быстрее.
ImageView
Вид на чужой буфер: библиотека его не копирует, не освобождает и не переживает — буфер обязан быть жив всё время вызова.
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 * каналов), а не «ноль байт в строке»: кадр камеры почти никогда не плотный — строки выравнивают, а вырезка наследует длину строки оригинала. Собирается на месте вызова без копирования:
const dmr::ImageView view{ m.data, m.cols, m.rows, (int)m.step, dmr::PixelFormat::Bgr8 };Image
Кадр, которым библиотека владеет сама — нужен там, где она его создаёт, то есть при чтении с диска. Неявно приводится к ImageView, поэтому передаётся в scan() напрямую, без .view():
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;
int channels() const;
bool empty() const;
uint8_t* data();
uint8_t* row(int y);
ImageView view() const;
operator ImageView() const; // неявно — для вызова scan(image)
};Чтение с диска
enum class LoadStatus {
Ok, // снимок прочитан
NotFound, // файла нет
Unreadable, // файл есть, но это не читаемый снимок
};
enum class LoadAs {
Color, // три канала, PixelFormat::Bgr8
Gray, // один канал, PixelFormat::Gray8
};
LoadStatus loadImage(const std::string& path, Image& out, LoadAs as = LoadAs::Color);Для сканера Gray — лучший выбор: снимок монохромной камеры (полутон в трёхканальном JPEG) в этом виде читается дешевле, а ответ побайтово тот же, потому что каждый канал Color и есть эта яркость. При отказе out очищается — половина кадра от прошлой попытки хуже пустого.
dmr::Image frame;
switch (dmr::loadImage("кадр.jpg", frame, dmr::LoadAs::Gray)) {
case dmr::LoadStatus::Ok: break;
case dmr::LoadStatus::NotFound: /* файла нет */ break;
case dmr::LoadStatus::Unreadable: /* файл есть, но это не снимок */ break;
}loadImage сама разбирается с системной кодировкой пути — каталог с русским именем не роняет чтение. Для собственной работы с путями в заголовке есть toPath(const std::string&), fromPath(const std::filesystem::path&) (обратное преобразование) и printablePath(...) — путь в UTF-8, для журнала.
Геометрия
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 — четыре его угла точнее, чем прямоугольником: на конвейере символ снят под углом.
for (const dmr::Point& p : c.corners.corner)
drawTo(p.x, p.y);Углы Quad идут по кругу, и под индексом 3 стоит тот, который детектор принял за угол сплошной L-рамки — решение, принятое по плотности сторон до всякого чтения. Окончательный поворот символа выбирается позже и с ним может не совпасть. Для разметки поверх кадра corner[3] годится, для выводов о содержимом символа — нет.
Настройки
Значения по умолчанию — не выбор наугад, а результат тестирования на большом наборе кадров. Начинать стоит с пресета, а не с отдельных полей — имя пресета уже говорит, под какой сценарий он настроен.
| Пресет | Для чего | Чем отличается |
|---|---|---|
Settings::stream() | Поток кадров одной этикетки — конвейер, камера. Умолчание | Подсказка размера по предыдущим кадрам включена, перебор размеров выключен |
Settings::single() | Разовые снимки — папка, ручная проверка | Подсказки нет, перебираются 24 размера символа |
Settings::multi(n) | Несколько кодов на одном кадре | maxCodes = n (0 — сколько найдётся); вчетверо дороже одиночного чтения |
auto s = dmr::Settings::stream();
s.budgetMs = 120.0; // 0 — без ограничения
s.requireGs1 = true; // принимать только коды структуры GS1
const dmr::Scanner scanner(s);Разница не стилистическая, а в скорости. single() перебирает до 24 размеров символа, если размер заранее не известен, — на кадре, который так и не удалось прочитать (отказном), это заметно медленнее, чем без перебора, хотя и поднимает долю успешных чтений на несколько процентов. На потоке кадров одной этикетки размер от кадра к кадру не меняется, поэтому перебор не даёт выигрыша в точности вообще — только платит временем на каждом отказе. Именно поэтому в stream() он выключен, а в single() включён: для разрозненных снимков, где размер заранее не знаешь, эта плата оправдана.
Все поля
| Поле | Умолчание | Смысл |
|---|---|---|
maxCodes | 1 | Сколько кодов искать: 1 — один, 0 — сколько найдётся, N — не больше N |
requireGs1 | false | Принимать только коды структуры GS1. Сошедшаяся коррекция Рида-Соломона сама по себе не доказывает верного чтения — проверка дополнительно смотрит на формат содержимого (AI 01, 14 цифр GTIN со сходящейся контрольной суммой, затем AI 21) |
forcedRotation | -1 | Поворот символа, 0..3 — четверти по часовой; -1 — определять самому |
budgetMs | 0.0 | Бюджет времени на кадр, мс; 0 — без ограничения. Срок мягкий: проверка идёт между стадиями перебора, а не внутри них, поэтому время кадра может превысить бюджет на длительность идущей стадии |
rowParallel | true | Делить строки ядер изображения между потоками пула, пока кадр идёт одним потоком. Снижает задержку кадра ценой процессорного времени — выключать, если на машине несколько сканеров или важнее пропускная способность, а не задержка |
maxSizeTries | 1 | Сколько размеров символа пробовать, когда размеру верить нельзя; 1 — не перебирать |
warpScales | пусто | Масштабы холста выпрямления для последнего прохода, пикселей на модуль; пусто — не перебирать. Разбирается из строки вида "7,8,10" методом setWarpScales() |
maxCandidates | 6 | Сколько кандидатов брать у детектора по L-паттерну за один проход |
readerCacheMb | 64.0 | Предел памяти, МБ на кадр, под повторное использование работы; 0 — ничего не хранить. На ответ не влияет |
mcHintRun | 5 | Сколько кадров подряд должны сойтись на одном размере символа, чтобы дальше он проверялся первым; 0 — не подсказывать |
maxPatternError | 0.25 | Предел доли ошибок в служебных модулях (L-рамка, timing), при котором результат вообще рассматривается |
lpatRetryScale | 0.8 | Повторный поиск по L-паттерну на точном масштабе, как вторая гипотеза; 0 — не повторять |
directSampling | false | Брать яркости модулей прямо из исходника, минуя холст выпрямления |
useLPatternFinder / useTextureFinder | true / true | Источники кандидатов; выключение нужно для сравнения детекторов на одном корпусе |
collectDetails | false | Собирать разбор прочитанного и отказа в ScanResult::details (около 2 МБ на результат) |
Диагностика
Библиотека не пишет в консоль и не читает переменные окружения — приёмник журнала передаётся явно, и без него строки даже не собираются:
using LogSink = std::function<void(std::string_view line)>;
void setLogSink(LogSink sink); // пустой — выключить
void setVerbose(bool on); // по умолчанию выключена
bool isVerbose(); // включена И есть приёмник
std::ostream& dbg(); // поток выкладки: в приёмник, иначе «в никуда»dmr::setLogSink([](std::string_view line) { std::cerr << line; });
dmr::setVerbose(true);Разбор причины конкретного чтения или отказа — отдельно и по запросу, строкой, а не в журнал. Собирается, только если при чтении был включён Settings::collectDetails:
struct ExplainOptions {
int moduleCount = 0; // размер, заданный при чтении; 0 — определялся сам
int forcedRotation = -1; // поворот, заданный при чтении
bool images = false; // сохранять отладочные PNG рядом с рабочим каталогом
std::string imagePath; // имя снимка — для подписей отладочных файлов
};
std::string explain(const Details& details, const ExplainOptions& opt = {});auto s = dmr::Settings::stream();
s.collectDetails = true; // около 2 МБ на результат
const dmr::ScanResult r = dmr::Scanner(s).scan(frame);
if (r.details) std::cerr << dmr::explain(*r.details);Статус кадра уже объявил сканер. Когда символ найден, но не прочитан, разбирать нечего — тогда вырезка заново выпрямляется и декодируется, чтобы отказу было чем объясниться, и это стоит примерно столько же, сколько само чтение кадра.
Версия библиотеки
#define DMR_VERSION_MAJOR 1
#define DMR_VERSION_MINOR 0
#define DMR_VERSION_PATCH 0
#define DMR_VERSION_STRING "1.0.0"
#define DMR_VERSION_AT(ma, mi, pa) ((ma) * 10000 + (mi) * 100 + (pa))
#define DMR_VERSION DMR_VERSION_AT(DMR_VERSION_MAJOR, DMR_VERSION_MINOR, DMR_VERSION_PATCH)
namespace dmr { std::string version(); }Макросы говорят про заголовок, с которым скомпилировались; dmr::version() — про библиотеку, с которой слинковались. Разойтись они могут: библиотека ставится в префикс и живёт там дольше, чем помнит собиравший.
#if DMR_VERSION >= DMR_VERSION_AT(1, 1, 0)
// код под заголовок 1.1.0 и новее
#endifЛицензия
Сканер читает кадры, пока действует пробный период (месяц с первого запуска на машине, без сети и регистрации) или лицензия. Иначе Scanner::scan() сразу отвечает Status::Unlicensed и кадр не разбирает. Лицензия привязана к оборудованию (материнская плата, процессор, системный диск), а не к установке ОС.
#include <dmr/License.h>
const dmr::LicenseStatus s = dmr::license::status();
if (!s.canScan()) show(s.message); // «Пробный период закончился. Нужна лицензия»
dmr::LicenseResult r = dmr::license::activate(keyFromUser); // «DMR-XXXXX-XXXXX-XXXXX-XXXXX»
if (!r.ok) show(r.message);LicenseState
| Значение | Смысл |
|---|---|
Trial | Идёт пробный период |
Active | Лицензия действует |
RefreshDue | Действует, но связи с сервером давно не было; автосверка повторяет попытки раз в час |
TrialExpired | Пробный период кончился, лицензии нет |
Expired | Оплаченный срок лицензии кончился; продление в личном кабинете автосверка подхватит сама |
NetworkRequired | Без связи с сервером дальше нельзя; как только сервер ответит, автосверка вернёт лицензию |
WrongMachine | Лицензия выдана другой машине |
Invalid | Запись лицензии не проходит проверку подписи |
LicenseStatus
| Поле | Смысл |
|---|---|
state | LicenseState |
canScan() | Читает ли сейчас сканер — true при Trial, Active или RefreshDue |
validUntil | Конец пробного периода или оплаченного срока, unix-секунды UTC |
refreshAfter | С какого момента токен считается устаревшим и лицензия переходит в RefreshDue; 0 — не устаревает |
offlineUntil | Дольше этого без связи с сервером лицензия не работает; 0 — без ограничения |
daysLeft | Целых суток до ближайшей из границ выше; 0 — граница пройдена |
licenseId / activationId | Идентификаторы лицензии и конкретной активации |
offlineMode | limited | extended | full; пусто без лицензии |
clockRollback | Системные часы отстают от времени, которое машина уже видела — откат часов срок не продлевает |
message | Состояние словами, для показа пользователю |
Активация: онлайн и офлайн
Онлайн — ключ из личного кабинета, машина сама идёт на сервер лицензий:
dmr::LicenseResult r = dmr::license::activate("DMR-XXXXX-XXXXX-XXXXX-XXXXX");Офлайн — для машины без интернета: файл запроса переносится на машину с сетью и загружается в личный кабинет, в ответ приходит license.lic:
dmr::license::writeActivationRequest("request.json"); // → загрузить в кабинет, получить license.lic
dmr::license::importLicenseFile("license.lic"); // тем же вызовом продлевается: новый файл заменяет старыйАктивированная по сети лицензия раз в сутки сама сверяется с сервером в фоновом потоке — обновляет токен, приносит отзыв и продление срока. Вызывать для этого ничего не нужно, а чтение кадра сеть не ждёт никогда; сервер недоступен — повтор через час, лицензия работает, пока не выйдет срок работы без связи.
Отвязка и остальные функции
namespace dmr::license {
void configure(const LicenseOptions& options); // до первого сетевого вызова; можно не звать
LicenseStatus status();
LicenseResult activate(const std::string& licenseKey);
LicenseResult refresh(); // сверить сейчас, не дожидаясь суток
LicenseResult deactivate(const std::string& reason = {}); // расписка уходит на сервер
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(); // для обращений в поддержку
}Переезд на другую машину — deactivate() на старой: место освобождается на сервере распиской, подписанной секретом этой машины. Без сети — deactivateToFile(): расписка ложится в файл, отправить его можно с любой машины. Все функции потокобезопасны; функции с сетью блокируют вызывающего до ответа или до истечения LicenseOptions::timeoutMs (по умолчанию 15000 мс).
Каждый ответ сервера лицензий привязан к своему запросу: библиотека отправляет случайное одноразовое число, сервер возвращает его в подписанном ответе, и записанный раньше ответ повторно не примется. Расписка о сдаче места тоже одноразовая — в ней номер из последнего полученного от сервера ответа, поэтому расписку из восстановленной копии старого состояния машины сервер не примет. Секрет для подписи расписок сервер выдаёт только при первой активации машины. У лицензий с полной автономией (full) сдача места расходует лимит переносов; когда он исчерпан, перенос делает поддержка.
Ключ активации для activate() выдаётся из личного кабинета после оформления тарифа в каталоге.
Командная строка
В поставку вместе с библиотекой входит готовая оболочка командной строки dmr — для быстрой проверки без своего кода: разовый снимок, папка, список путей или поток для внешнего сервиса.
dmr <снимок> [число_модулей] [--verbose] [--rot 0..3] [--budget-ms N] [--codes N]
dmr --batch <список> [число_модулей] [--verbose]
dmr --folder <каталог> [число_модулей] [--verbose]
dmr --serve [число_модулей] [--verbose]
dmr --license status | activate <ключ> | refresh | deactivate | fingerprint [--json]Разовый снимок
$ dmr этикетка.jpg
запуск: 4.6 мс
обработка: 24.9 мс
код: 0104627191145677215n5Gr,pHTGIU&<GS>93CuXUПакетный режим и поток
--batch и --folder — один процесс на весь заранее известный набор снимков, результат построчно на stdout: путь · код возврата · миллисекунды · текст.
--serve — тот же формат строки, но снимки не известны заранее: путь приходит по одному, по строке из stdin, и обрабатывается сразу же — процесс держит сканер живым между запросами внешнего сервиса, не платя за инициализацию на каждый. Результат сбрасывается сразу (std::flush), не дожидаясь, пока закроют весь ввод, — так демо-раздел этого сайта (/scan) держит один процесс на все запросы, вместо процесса на снимок.
$ mkfifo queue
$ dmr --serve < queue &
$ echo /путь/к/снимку.jpg > queue
/путь/к/снимку.jpg 0 91.2 0104627191145677215n5Gr,pHTGIU&<GS>93CuXUЛицензия: машиночитаемый вывод
У всех подкоманд --license есть флаг --json — тот же результат, что и в человекочитаемом выводе, но в виде одной JSON-строки на stdout, для скриптов и админ-панелей:
$ dmr --license status --json
{"state":"trial","can_scan":true,"valid_until":1792831857,"refresh_after":0,
"offline_until":0,"days_left":28,"license_id":"","activation_id":"",
"offline_mode":"","clock_rollback":false,"message":"Пробный период: осталось 28 дн."}Поле state — устойчивый машиночитаемый код (trial, active, refresh_due, trial_expired, expired, network_required, wrong_machine, invalid) — часть формата вывода, не переводится и не меняется словами.
Нужной функции нет в библиотеке или ваш случай за её границами — доработаем под вашу задачу. Напишите нам →